Popup de compra rápida

En este tutorial vamos a ver cómo agregar un popup con el formulario de producto, el cual puede ser visto desde el ítem en el listado de productos.

HTML

1. Lo primero que vamos a hacer, es agregar un nuevo snipplet llamado quick-shop.tpl dentro de la carpeta snipplets/grid con el siguiente código:

{% if settings.quick_shop %}
    {% embed "snipplets/modal.tpl" with{modal_id: 'quickshop-modal', modal_class: 'quickshop text-center', modal_position: 'bottom modal-bottom-sheet', modal_transition: 'slide', modal_header: true, modal_footer: true, modal_width: 'centered modal-docked-md modal-docked-md-centered', modal_mobile_full_screen: 'true' } %}
        {% block modal_body %}
        <div class="js-item-product" data-product-id="">
            <div class="js-product-container js-quickshop-container js-quickshop-modal js-quickshop-modal-shell" data-variants="" data-quickshop-id="">
                <div class="js-item-variants">
                    <div class="js-item-name h1 mb-1" data-store="product-item-name-{{ product.id }}"></div>
                    <div class="item-price-container mb-4" data-store="product-item-price-{{ product.id }}">
                        <span class="js-compare-price-display h4 price-compare"></span>
                        <span class="js-price-display h4"></span>
                    </div>
                    {# Image is hidden but present so it can be used on cart notification #}
                    <img srcset="" class="js-quickshop-img hidden"/>
                    <div id="quickshop-form"></div>
                </div>
            </div>
        </div>
        {% endblock %}
    {% endembed %}
{% endif %}

2. Luego, vamos a buscar el snipplet item.tpl dentro de la carpeta snipplets/grid, puede ser que en tu diseño este snipplet se llame single_product.tpl y usamos el siguiente código:

{% set slide_item = slide_item | default(false) %}
{% set columns = settings.grid_columns %}
{% set has_color_variant = false %}
{% if settings.product_color_variants %}
    {% for variation in product.variations if variation.name in ['Color', 'Cor'] and variation.options | length > 1 %}
        {% set has_color_variant = true %}
    {% endfor %}
{% endif %}
 
<div class="js-item-product {% if slide_item %}js-item-slide swiper-slide{% else %}col{% if columns == 2 %}-6 col-md-3{% else %}-12 col-md-4{% endif %}{% endif %} item item-product{% if not product.display_price %} no-price{% endif %}" data-product-type="list" data-product-id="{{ product.id }}" data-store="product-item-{{ product.id }}">
 
    {% if settings.quick_shop or settings.product_color_variants %}
        <div class="js-product-container js-quickshop-container {% if product.variations %}js-quickshop-has-variants{% endif %}" data-variants="{{ product.variants_object | json_encode }}" data-quickshop-id="quick{{ product.id }}{% if slide_item and section_name %}-{{ section_name }}{% endif %}">
    {% endif %}
 
        {% set product_url_with_selected_variant = has_filters ?  ( product.url | add_param('variant', product.selected_or_first_available_variant.id)) : product.url  %}
 
        {% if has_color_variant %}
 
            {# Item image will be the first avaiable variant #}
 
            {% set item_img_spacing = product.featured_variant_image.dimensions['height'] / product.featured_variant_image.dimensions['width'] * 100 %}
            {% set item_img_srcset = product.featured_variant_image %}
            {% set item_img_alt = product.featured_variant_image.alt %}
        {% else %}
 
            {# Item image will be the first image regardless the variant #}
 
            {% set item_img_spacing = product.featured_image.dimensions['height'] / product.featured_image.dimensions['width'] * 100 %}
            {% set item_img_srcset = product.featured_image %}
            {% set item_img_alt = product.featured_image.alt %}
        {% endif %}
 
        <div class="js-item-image item-image mb-2">
            <div style="padding-bottom: {{ item_img_spacing }}%;" class="p-relative" data-store="product-item-image-{{ product.id }}">
                <a href="{{ product_url_with_selected_variant }}" title="{{ product.name }}">
                    <img alt="{{ item_img_alt }}" data-sizes="auto" data-expand="-10" src="{{ 'images/empty-placeholder.png' | static_url }}" data-srcset="{{ item_img_srcset | product_image_url('small')}} 240w, {{ item_img_srcset | product_image_url('medium')}} 320w, {{ item_img_srcset | product_image_url('large')}} 480w" class="lazyautosizes lazyload img-absolute img-absolute-centered fade-in" /> 
                    <div class="placeholder-fade"></div>
                </a>
                {% if settings.product_color_variants %}
                    {% include 'snipplets/labels.tpl' with {color: true} %}
                    {% include 'snipplets/grid/item-colors.tpl' %}
                {% else %}
                    {% include 'snipplets/labels.tpl' %}
                {% endif %}
            </div>
        </div>
        {% if (settings.quick_shop or settings.product_color_variants) and product.variations %}
 
            {# Hidden product form to update item image and variants: Also this is used for quickshop popup #}
            
            <div class="js-item-variants hidden">
                <form id="product_form" class="js-product-form" method="post" action="{{ store.cart_url }}">
                    <input type="hidden" name="add_to_cart" value="{{product.id}}" />
                    {% if product.variations %}
                        {% include "snipplets/product/product-variants.tpl" with {quickshop: true} %}
                    {% endif %}
                    {% if product.available and product.display_price and settings.quick_shop %}
                        {% include "snipplets/product/product-quantity.tpl" with {quickshop: true} %}
                    {% endif %}
                    {% set state = store.is_catalog ? 'catalog' : (product.available ? product.display_price ? 'cart' : 'contact' : 'nostock') %}
                    {% set texts = {'cart': "Agregar al carrito", 'contact': "Consultar precio", 'nostock': "Sin stock", 'catalog': "Consultar"} %}
 
                    {# Add to cart CTA #}
 
                    <input type="submit" class="js-addtocart js-prod-submit-form btn btn-primary btn-block {{ state }}" value="{{ texts[state] | translate }}" {% if state == 'nostock' %}disabled{% endif %} />
 
                    {# Fake add to cart CTA visible during add to cart event #}
 
                    {% include 'snipplets/placeholders/button-placeholder.tpl' with {custom_class: "btn-block"} %}
 
                </form>
            </div>
 
        {% endif %}
        <div class="item-description" data-store="product-item-info-{{ product.id }}">
            <a href="{{ product_url_with_selected_variant }}" title="{{ product.name }}" class="item-link">
                <div class="js-item-name item-name mb-1" data-store="product-item-name-{{ product.id }}">{{ product.name }}</div>
                {% if product.display_price %}
                    <div class="item-price-container mb-1" data-store="product-item-price-{{ product.id }}">
                        <span class="js-compare-price-display price-compare" {% if not product.compare_at_price or not product.display_price %}style="display:none;"{% else %}style="display:inline-block;"{% endif %}>
                            {{ product.compare_at_price | money }}
                        </span>
                        <span class="js-price-display item-price">
                            {{ product.price | money }}
                        </span>
 
                    </div>
                {% endif %}
            </a>
        </div>
        {% include 'snipplets/payments/installments.tpl' %}
 
        {% if settings.quick_shop and product.available and product.display_price %}
 
            {# Trigger quickshop actions #}
            
            <div class="item-actions mt-2">
                {% if product.variations %}
 
                    {# Open quickshop popup if has variants #}
 
                    <a data-toggle="#quickshop-modal" data-modal-url="modal-fullscreen-quickshop" class="js-quickshop-modal-open {% if slide_item %}js-quickshop-slide{% endif %} js-modal-open js-fullscreen-modal-open btn btn-primary btn-small px-4" title="{{ 'Compra rápida de' | translate }} {{ product.name }}" aria-label="{{ 'Compra rápida de' | translate }} {{ product.name }}" >{{ 'Agregar al carrito' | translate }}</a>
                {% else %}
 
                    {# If not variants add directly to cart #}
                    <form id="product_form" class="js-product-form" method="post" action="{{ store.cart_url }}">
                        <input type="hidden" name="add_to_cart" value="{{product.id}}" />
                        {% set state = store.is_catalog ? 'catalog' : (product.available ? product.display_price ? 'cart' : 'contact' : 'nostock') %}
                        {% set texts = {'cart': "Agregar al carrito", 'contact': "Consultar precio", 'nostock': "Sin stock", 'catalog': "Consultar"} %}
 
                        <input type="number" name="quantity" value="1" class="js-quantity-input hidden" aria-label="{{ 'Cambiar cantidad' | translate }}" >
 
                        <input type="submit" class="js-addtocart js-prod-submit-form btn btn-primary btn-small {{ state }} px-4 mb-1" value="{{ texts[state] | translate }}" {% if state == 'nostock' %}disabled{% endif %} />
 
                        {# Fake add to cart CTA visible during add to cart event #}
 
                        {% include 'snipplets/placeholders/button-placeholder.tpl' with {custom_class: "js-addtocart-placeholder-inline btn-small mb-1"} %}
 
                    </form>
                {% endif %}
            </div>
        {% endif %}
 
        {# Structured data to provide information for Google about the product content #}
        {% include 'snipplets/structured_data/item-structured-data.tpl' %}
    {% if settings.quick_shop or settings.product_color_variants %}
        </div>
    {% endif %}
</div>

3.  Vamos a crear un nuevo snipplet llamado button-placeholder.tpl dentro de la carpeta snipplets/placeholders para incluir las transiciones del botón al “Agregar al carrito”. El código es el siguiente:

<div class="js-addtocart js-addtocart-placeholder btn btn-primary btn-transition disabled {{ custom_class }}" style="display: none;">
    <span class="js-addtocart-text transition-container btn-transition-start active">
        {{ 'Agregar al carrito' | translate }}
    </span>
    <span class="js-addtocart-success transition-container btn-transition-success">
        {{ '¡Listo!' | translate }}
    </span>
    <div class="js-addtocart-adding transition-container btn-transition-progress">
        {{ 'Agregando...' | translate }}
    </div>
</div>

4. Dentro de la carpeta snipplets/product, vamos a editar 2 archivos. Por un lado, el snipplet product-quantity.tpl con el siguiente código.

{% if not quickshop %}
    <div class="row">
        <div class="col col-md-4">
{% endif %}
{% embed "snipplets/forms/form-input.tpl" with{type_number: true, input_value: '1', input_name: 'quantity' ~ item.id, input_custom_class: 'js-quantity-input text-center', input_label: false, input_append_content: true, input_group_custom_class: 'js-quantity form-row align-items-center', form_control_container_custom_class: 'col-6', input_min: '1', input_aria_label: 'Cambiar cantidad' | translate } %}
    {% block input_prepend_content %}
        <span class="js-quantity-down col-3 text-center">
            {% include "snipplets/svg/minus.tpl" with {svg_custom_class: "icon-inline icon-w-12 icon-lg svg-icon-text"} %}
        </span>
    {% endblock input_prepend_content %}
    {% block input_append_content %}
        <span class="js-quantity-up col-3 text-center">
            {% include "snipplets/svg/plus.tpl" with {svg_custom_class: "icon-inline icon-w-12 icon-lg svg-icon-text"} %}
        </span>
    {% endblock input_append_content %}
{% endembed %}
{% if not quickshop %}
        </div>
    </div>
{% endif %}

Y por otro, el product-variants.tpl con el siguiente código.

<div class="js-product-variants{% if quickshop %} js-product-quickshop-variants text-left{% endif %} form-row">
    {% for variation in product.variations %}
        <div class="js-product-variants-group {% if variation.name in ['Color', 'Cor'] %}js-color-variants-container{% endif %} {% if loop.length == 3 %} {% if quickshop %}col-4{% else %}col-12{% endif %} col-md-4 {% elseif loop.length == 2 %} col-6 {% else %} col {% if quickshop %}col-md-12{% else %}col-md-6{% endif %}{% endif %}">
            {% embed "snipplets/forms/form-select.tpl" with{select_label: true, select_label_name: '' ~ variation.name ~ '', select_for: 'variation_' ~ loop.index , select_data: 'variant-id', select_data_value: 'variation_' ~ loop.index, select_name: 'variation' ~ '[' ~ variation.id ~ ']', select_custom_class: 'js-variation-option js-refresh-installment-data'} %}
                {% block select_options %}
                    {% for option in variation.options %}
                        <option value="{{ option.id }}" {% if product.default_options[variation.id] == option.id %}selected="selected"{% endif %}>{{ option.name }}</option>
                    {% endfor %}
                {% endblock select_options%}
            {% endembed %}
        </div>
    {% endfor %}
</div>

5. Agregamos un parámetro de select_data en el snipplet form-select.tpl dentro de la carpeta snipplets/forms. El código quedaría asi:

<div class="form-group {{ select_group_custom_class }}">
    {% if select_label %}
        <label {% if select_label_id%}id="{{ select_label_id }}"{% endif %} class="form-label {{ select_label_custom_class }}" {% if select_for %}for="{{ select_for }}"{% endif %}>{{ select_label_name }}</label>
    {% endif %}
    <select 
        {% if select_id %}id="{{ select_id }}"{% endif %}
        class="form-select {{ select_custom_class }} {% if select_inline %}form-control-inline{% endif %}"
        {% if select_data %}data-{{select_data}}="{{select_data_value}}"{% endif %}
        {% if select_name %}name="{{ select_name }}"{% endif %}
        {% if select_aria_label %}aria-label="{{ select_aria_label }}"{% endif %}>
        {% block select_options %}
        {% endblock select_options %}
    </select>
    <div class="form-select-icon">
        {% include "snipplets/svg/chevron-down.tpl" with {svg_custom_class: "icon-inline icon-w-14 icon-lg svg-icon-text"} %}
    </div>
</div>

6. Ahora necesitamos crear el snipplet para el componente modal o popup dentro de la carpeta snipplets. Este tpl se llama modal.tpl y el código es:

{# /*============================================================================
  #Modal
==============================================================================*/
 
#Properties
    // ID
    // Position - Top, Right, Bottom, Left
    // Transition - Slide and Fade
    // Width - Full and Box
    // modal_form_action - For modals that has a form
 
 
#Head
    // Block - modal_head
#Body
    // Block - modal_body
#Footer
    // Block - modal_footer
 
#}
 
{% set modal_overlay = modal_overlay | default(true) %}
 
<div id="{{ modal_id }}" class="js-modal {% if modal_mobile_full_screen %}js-fullscreen-modal{% endif %} modal modal-{{ modal_class }} modal-{{modal_position}} transition-{{modal_transition}} modal-{{modal_width}} transition-soft" style="display: none;">
    {% if modal_form_action %}
    <form action="{{ modal_form_action }}" method="post" class="{{ modal_form_class }}" {% if modal_form_hook %}data-store="{{ modal_form_hook }}"{% endif %}>
    {% endif %}
    <div class="js-modal-close {% if modal_mobile_full_screen %}js-fullscreen-modal-close{% endif %} modal-header">
        <span class="modal-close">
            {% include "snipplets/svg/times.tpl" with {svg_custom_class: "icon-inline svg-icon-text"} %}
        </span>
        {% block modal_head %}{% endblock %}
    </div>
    <div class="modal-body">
        {% block modal_body %}{% endblock %}
    </div>
    {% if modal_footer %}
        <div class="modal-footer d-md-block">
            {% block modal_foot %}{% endblock %}
        </div>
    {% endif %}
    {% if modal_form_action %}
    </form>
    {% endif %}
</div>

7. Por último, en layout.tpl agregamos el siguiente código para que levante el contenido del popup:

{# Quickshop modal #}
{% snipplet "grid/quick-shop.tpl" %}

CSS

Requisito:

Tener agregados en tu diseño las clases helpers. Podés seguir este este pequeño tutorial para hacerlo (simplemente es copiar y pegar algunas clases, no toma más de 1 minuto).

1. Agregamos los estilos dentro del archivo static/style-async.tpl

Si en tu diseño usas una hoja de estilos para CSS asíncrono, vamos a necesitar el siguiente código dentro de la misma, pero si no es el caso entonces podés agregarlo en la hoja de tu CSS principal.

{# /* // Buttons */ #}
.btn-transition {
  position: relative;
  overflow: hidden;
  .transition-container {
    position: absolute;
    top: 50%;
    left: 0;
    width: 100%;
    margin-top: -7px;
    opacity: 0;
    text-align: center;
    @include prefix(transition, all 0.5s ease, webkit ms moz o);
    cursor: not-allowed;
    pointer-events: none;
    &.active {
      opacity: 1;
    }
  }
}
{# /* // Modals */ #}
.modal {
  position: fixed;
  top: 0;
  display: block;
  width: 80%;
  height: 100%;
  padding: 10px;
  -webkit-overflow-scrolling: touch;
  overflow-y: auto;
  transition: all .2s cubic-bezier(.16,.68,.43,.99);
  z-index: 20000;
  &-header{
    width: calc(100% + 20px);
    margin: -10px 0 10px -10px;
    padding: 10px 15px;
    font-size: 20px;
  }
  &-footer{
    padding: 10px;
    clear: both;
  }
  &-full {
    width: 100%;
  }
  &-docked-md{
    width: 100%;
  }
  &-docked-small{
    width: 80%;
  }
  &-top{
    top: -100%;
    left: 0;
  }
  &-bottom{
    top: 100%;
    left: 0;
  }
  &-left{
    left: -100%;
  }
  &-right{
    right: -100%;
  }
  &-centered{
    height: 100%;
    width: 100%;
  }
  &-top.modal-show,
  &-bottom.modal-show {
    top: 0;
  }
  &-bottom-sheet {
    top: initial;
    bottom: -100%;
    height: auto;
    &.modal-show {
      top: initial;
      bottom: 0;
      height: auto;
    }
  }
  &-left.modal-show {
    left: 0;
  }
  &-right.modal-show {
    right: 0;
  }
  &-close { 
    display: inline-block;
    padding: 1px 5px 5px 0;
    margin-right: 5px;
    vertical-align: middle;
    cursor: pointer;
  }
  .tab-group{
    margin:  0 -10px 20px -10px;
  }
}
 
.modal-overlay{
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #00000047;
  z-index: 10000;
}
@media (min-width: 768px) { 
 
  {# /* Modals */ #}
 
  .modal{
    &-centered{
      height: 80%;
      width: 80%;
      left: 10%;
      margin: 5% auto;
    }
    &-docked-md{
      width: 500px;
      &-centered{
        left: calc(50% - 250px);
        bottom: auto;
        height: auto;
      }
    }
    &-bottom-sheet {
      top: 100%;
      &.modal-show {
        top: 0;
        bottom: auto;
      }
    }
    &-docked-small{
      width: 350px;
    }
  }
}

JS

1. El JavaScript necesitamos agregarlo en el archivo store.js.tpl (o donde tengas tus funciones de JS).  Agregamos el siguiente código :

El JS para que funcionen los modals

{#/*============================================================================
      #Modals
    ==============================================================================*/ #}
 
    {% if settings.quick_shop %}
 
        restoreQuickshopForm = function(){
 
            {# Restore form to item when quickshop closes #}
 
            {# Clean quickshop modal #}
 
            $("#quickshop-modal .js-item-product").removeClass("js-swiper-slide-visible js-item-slide");
            $("#quickshop-modal .js-quickshop-container").attr( { 'data-variants' : '' , 'data-quickshop-id': '' } );
            $("#quickshop-modal .js-item-product").attr('data-product-id', '');
 
            {# Wait for modal to become invisible before removing form #}
            
            setTimeout(function(){
                var $quickshop_form = $("#quickshop-form").find('.js-product-form');
                var $item_form_container = $(".js-quickshop-opened").find(".js-item-variants");
                
                $quickshop_form.detach().appendTo($item_form_container);
                $(".js-quickshop-opened").removeClass("js-quickshop-opened");
            },350);
 
        };
 
    {% endif %}
    
    {# Full screen mobile modals back events #}
 
    if ($(window).width() < 768) {
 
        {# Clean url hash function #}
 
        cleanURLHash = function(){
            const uri = window.location.toString();
            const clean_uri = uri.substring(0, uri.indexOf("#"));
            window.history.replaceState({}, document.title, clean_uri);
        };
 
        {# Go back 1 step on browser history #}
 
        goBackBrowser = function(){
            cleanURLHash();
            history.back();
        };
 
        {# Clean url hash on page load: All modals should be closed on load #}
 
        if(window.location.href.indexOf("modal-fullscreen") > -1) {
            cleanURLHash();
        }
 
        {# Open full screen modal and url hash #}
 
        $(document).on("click", ".js-fullscreen-modal-open", function(e) {
            e.preventDefault();
            var modal_url_hash = $(this).data("modal-url");            
            window.location.hash = modal_url_hash;
        });
 
        {# Close full screen modal: Remove url hash #}
 
        $(document).on("click", ".js-fullscreen-modal-close", function(e) {
            e.preventDefault();
            goBackBrowser();
        });
 
        {# Hide panels or modals on browser backbutton #}
 
        window.onhashchange = function() {
            if(window.location.href.indexOf("modal-fullscreen") <= -1) {
 
                {# Close opened modal #}
 
                if($(".js-fullscreen-modal").hasClass("modal-show")){
 
                    var $opened_modal = $(".js-fullscreen-modal.modal-show");
                    var $opened_modal_overlay = $opened_modal.prev();
 
                    $opened_modal.removeClass("modal-show").delay(500).hide(0);
                    $opened_modal_overlay.fadeOut(500);
 
                    {% if settings.quick_shop %}
                        restoreQuickshopForm();
                    {% endif %}
                }
            }
        }
 
    }
    
    $(document).on("click", ".js-modal-open", function(e) {
        e.preventDefault(); 
        var $modal_id = $(this).data('toggle');
        $(".js-modal-overlay").fadeToggle();
        if ($($modal_id).hasClass("modal-show")) {
            $($modal_id).removeClass("modal-show").delay(200).hide(0);
        } else {
            $($modal_id).detach().insertAfter(".js-modal-overlay").show(0).addClass("modal-show");
        }             
    });
 
    closeModal = function(element){
 
        $(element).closest(".js-modal").removeClass("modal-show").delay(200).hide(0); 
        $(".js-modal-overlay").fadeOut(300);
 
        {# Close full screen modal: Remove url hash #}
 
        if (($(window).width() < 768) && ($(element).hasClass(".js-fullscreen-modal-close"))) {
            goBackBrowser();
        }
 
        {% if settings.quick_shop %}
            restoreQuickshopForm();
        {% endif %}
        
    };
 
    $(document).on("click", ".js-modal-close", function(e) {
        e.preventDefault();  
        closeModal($(this));    
    });
 
    {# Close modal on ESC keyboard #}
 
    $(document).keyup(function(e) {
        if (e.keyCode == 27) {
            closeModal($(".js-modal-close"));    
        }
    });
 
    $(".js-modal-overlay").click(function (e) {
        e.preventDefault();  
        $(".js-modal.modal-show").removeClass("modal-show").delay(200).hide(0);   
        $(this).fadeOut(300);  
 
        {% if settings.quick_shop %}
            restoreQuickshopForm();
        {% endif %}
 
        if ($(window).width() < 768) {
            cleanURLHash();
        }
    });

El JS para actualizar la información del producto al cambiar de variante:

$(document).on("change", ".js-variation-option", function(e) {


        var $parent = $(this).closest(".js-product-variants");
        var $variants_group = $(this).closest(".js-product-variants-group");
        var $quickshop_parent_wrapper = $(this).closest(".js-quickshop-container");


        {# If quickshop is used from modal, use quickshop-id from the item that opened it #}
        
        var quick_id = $quickshop_parent_wrapper.attr("data-quickshop-id");


        if($parent.hasClass("js-product-quickshop-variants")){


            var $quickshop_parent = $(this).closest(".js-item-product");


            {# Target visible slider item if necessary #}
            
            if($quickshop_parent.hasClass("js-item-slide")){
                var $quickshop_variant_selector = '.js-swiper-slide-visible .js-quickshop-container[data-quickshop-id="'+quick_id+'"]';
            }else{
                var $quickshop_variant_selector = '.js-quickshop-container[data-quickshop-id="'+quick_id+'"]';
            }
            
            LS.changeVariant(changeVariant, $quickshop_variant_selector);


            {% if settings.product_color_variants %}
                {# Match selected color variant with selected quickshop variant #}


                if(($variants_group).hasClass("js-color-variants-container")){
                    var selected_option_id = $(this).find("option:selected").val();
                    if($quickshop_parent.hasClass("js-item-slide")){
                        var $color_parent_to_update = $('.js-swiper-slide-visible .js-quickshop-container[data-quickshop-id="'+quick_id+'"]');
                    }else{
                        var $color_parent_to_update = $('.js-quickshop-container[data-quickshop-id="'+quick_id+'"]');
                    }
                    $color_parent_to_update.find('.js-color-variant').removeClass("selected");
                    $color_parent_to_update.find('.js-color-variant[data-option="'+selected_option_id+'"]').addClass("selected");
                }
            {% endif %} 
        } else {
            LS.changeVariant(changeVariant, '#single-product');
        }


        var $this_compare_price =  $(this).closest(".js-product-container").find(".js-compare-price-display");
        var $this_price = $(this).closest(".js-product-container").find(".js-price-display");
        var $installment_container = $(this).closest(".js-product-container").find(".js-product-payments-container");
        var $installment_text = $(this).closest(".js-product-container").find(".js-max-installments-container");
        var $this_product_container = $(this).closest(".js-product-container");
        var $this_add_to_cart =  $(this).closest(".js-product-container").find(".js-prod-submit-form");
        // Get the current product discount percentage value
        var current_percentage_value = $this_product_container.find(".js-offer-percentage");
        // Get the current product price and promotional price
        var compare_price_value = $this_compare_price.html();
        var price_value = $this_price.html();
        // Filter prices to only have numbers
        old_price_value_filtered = parseInt(compare_price_value.replace(/[^0-9]/gi, ''), 10)/100;
        current_price_value_filtered = parseInt(price_value.replace(/[^0-9]/gi, ''), 10)/100;
        // Calculate new discount percentage based on difference between filtered old and new prices
        price_difference = (old_price_value_filtered-current_price_value_filtered);
        updated_discount_percentage = Math.round(((price_difference*100)/old_price_value_filtered));
        $this_product_container.find(".js-offer-percentage").html(updated_discount_percentage);
        if ($this_compare_price.css("display") == "none") {
            $this_product_container.find(".js-offer-label").hide();
        }
        else {
            $this_product_container.find(".js-offer-label").css("display" , "table");
        }
        if ($this_add_to_cart.hasClass("nostock")) {
            $this_product_container.find(".js-stock-label").show();
        }
        else {
            $this_product_container.find(".js-stock-label").hide();
        }
        if ($this_price.css('display') == 'none'){
            $installment_container.hide();
            $installment_text.hide();
        }else{
            $installment_text.show();
        }
    });

Luego, el JS para que el modal levante el contenido correspondiente y además quede sincronizado con el feature de Colores en el item de productos si es que lo tiene:

{% if settings.product_color_variants %}


        {# Product color variations #}


        $(document).on("click", ".js-color-variant", function(e) {
            e.preventDefault();
            $this = $(this);


            var option_id = $this.data('option');
            $selected_option = $this.closest('.js-item-product').find('.js-variation-option option').filter(function() {
                return this.value == option_id;
            });
            $selected_option.prop('selected', true).trigger('change');
            var available_variant = $(this).closest(".js-quickshop-container").data('variants');


            var available_variant_color = $(this).closest('.js-color-variant-active').data('option');


            for (var variant in available_variant) {
                if (option_id == available_variant[variant]['option'+ available_variant_color ]) {


                    if (available_variant[variant]['stock'] == null || available_variant[variant]['stock'] > 0 ) {


                        var otherOptions = getOtherOptionNumbers(available_variant_color);


                        var otherOption = available_variant[variant]['option' + otherOptions[0]];
                        var anotherOption = available_variant[variant]['option' + otherOptions[1]];


                        changeSelect($(this), otherOption, otherOptions[0]);
                        changeSelect($(this), anotherOption, otherOptions[1]);
                        break;


                    }
                }
            }
            $this.siblings().removeClass("selected");
            $this.addClass("selected");
        });


        function getOtherOptionNumbers(selectedOption) {
            switch (selectedOption) {
                case 0:
                    return [1, 2];
                case 1:
                    return [0, 2];
                case 2:
                    return [0, 1];
            }
        }


        function changeSelect(element, optionToSelect, optionIndex) {
            if (optionToSelect != null) {
                var selected_option_parent_id = element.closest('.js-item-product').data("product-id");
                var selected_option_attribute = $('.js-item-product[data-product-id="'+selected_option_parent_id+'"]').find('.js-color-variant-available-' + (optionIndex + 1)).data('value');
                var selected_option = $('.js-item-product[data-product-id="'+selected_option_parent_id+'"]').find('.js-variation-option[data-variant-id="'+selected_option_attribute+'"] option').filter(function() {
                    return this.value == optionToSelect;
                });


                selected_option.prop('selected', true).trigger('change');
            }
        }


    {% endif %}


    {% if settings.product_color_variants or settings.quick_shop %}
    
        {# Product quickshop for color variations #}


        LS.registerOnChangeVariant(function(variant){
            {# Show product image on color change #}
            
            var $item_to_update_image = $('.js-item-product[data-product-id^="'+variant.product_id+'"].js-swiper-slide-visible');
            var $item_to_update_image_cloned = $('.js-item-product[data-product-id^="'+variant.product_id+'"].js-swiper-slide-visible.swiper-slide-duplicate');


            {# If item is cloned from swiper change only cloned item #}


            if($item_to_update_image.hasClass("swiper-slide-duplicate")){
                var slide_item_index = $item_to_update_image_cloned.attr("data-swiper-slide-index");
                var current_image = $('img', '.js-item-product[data-product-id="'+variant.product_id+'-clone-'+slide_item_index+'" ]');
            }else{
                var slide_item_index = $item_to_update_image.attr("data-swiper-slide-index");
                var current_image = $('img', '.js-item-product[data-product-id="'+variant.product_id+'"]');
            }
            current_image.attr('srcset', variant.image_url);
        });


        
    {% endif %}


    {% if settings.quick_shop %}
        
        $(document).on("click", ".js-quickshop-modal-open", function (e) {
            e.preventDefault();
            var $this = $(this);
            if($this.hasClass("js-quickshop-slide")){
                $("#quickshop-modal .js-item-product").addClass("js-swiper-slide-visible js-item-slide");
            }
            LS.fillQuickshop($this);
        });


        {# Get width of the placeholder button #}
        var productButttonWidth = $(".js-addtocart-placeholder-inline").prev(".js-addtocart").innerWidth();
        $(".js-addtocart-placeholder-inline").width(productButttonWidth-20);
    {% endif %}

Por último, el JS que actualiza el botón y la notificación al agregar al carrito:

{# /* // Add to cart */ #}


    $(document).on("click", ".js-addtocart:not(.js-addtocart-placeholder)", function (e) {


        {# Button variables for transitions on add to cart #}


        var $productContainer = $(this).closest('.js-product-container');
        var $productVariants = $productContainer.find(".js-variation-option");
        var $productButton = $productContainer.find("input[type='submit'].js-addtocart");
        var $productButtonPlaceholder = $productContainer.find(".js-addtocart-placeholder");
        var $productButtonText = $productButtonPlaceholder.find(".js-addtocart-text");
        var $productButtonAdding = $productButtonPlaceholder.find(".js-addtocart-adding");
        var $productButtonSuccess = $productButtonPlaceholder.find(".js-addtocart-success");
        var productButttonHeight = $productButton.height();


        {# Define if event comes from quickshop or product page #}


        var isQuickShop = $productContainer.hasClass('js-quickshop-container');


        if (!isQuickShop) {
            if($(".js-product-slide-img.js-active-variant").length) {
                var imageSrc = $($productContainer.find('.js-product-slide-img.js-active-variant')[0]).data('srcset').split(' ')[0];
            } else {
                var imageSrc = $($productContainer.find('.js-product-slide-img')[0]).attr('srcset').split(' ')[0];
            }
            var name = $productContainer.find('.js-product-name').text();
            var price = $productContainer.find('.js-price-display').text();
        } else {
            var imageSrc = $(this).closest('.js-quickshop-container').find('img').attr('srcset');
            var name = $productContainer.find('.js-item-name').text();
            var price = $productContainer.find('.js-price-display').text().trim(); 
        }


        var quantity = $productContainer.find('.js-quantity-input').val();
        var addedToCartCopy = "{{ 'Agregar al carrito' | translate }}";




        if (!$(this).hasClass('contact')) {


            {% if settings.ajax_cart %}
                e.preventDefault();
            {% endif %}


            {# Hide real button and show button placeholder during event #}


            $productButton.hide();
            $productButtonPlaceholder.show().addClass("active");
            $productButtonPlaceholder.height(productButttonHeight);
            $productButtonText.removeClass("active");
            setTimeout(function(){
                $productButtonAdding.addClass("active");
            },300);


            {% if settings.ajax_cart %}


                var callback_add_to_cart = function(){


                    {# Animate cart amount #}


                    $(".js-cart-widget-amount").addClass("beat");


                    setTimeout(function(){
                        $(".js-cart-widget-amount").removeClass("beat");
                    },4000);


                    {# Fill notification info #}


                    $('.js-cart-notification-item-img').attr('srcset', imageSrc);
                    $('.js-cart-notification-item-name').text(name);
                    $('.js-cart-notification-item-quantity').text(quantity);
                    $('.js-cart-notification-item-price').text(price);


                    if($productVariants.length){
                        var output = [];


                        $productVariants.each( function(){  
                            var variants = $(this);
                            output.push(variants.val());
                        });
                        $(".js-cart-notification-item-variant-container").show();
                        $(".js-cart-notification-item-variant").text(output.join(', '))
                    }else{
                        $(".js-cart-notification-item-variant-container").hide();
                    }


                    {# Set products amount wording visibility #}


                    var cartItemsAmount = $(".js-cart-widget-amount").first().text();


                    if(cartItemsAmount > 1){
                        $(".js-cart-counts-plural").show();
                        $(".js-cart-counts-singular").hide();
                    }else{
                        $(".js-cart-counts-singular").show();
                        $(".js-cart-counts-plural").hide();
                    }


                    {# Show button placeholder with transitions #}


                    $productButtonAdding.removeClass("active");


                    setTimeout(function(){
                        $productButtonSuccess.addClass("active");
                    },300);
                    setTimeout(function(){
                        $productButtonSuccess.removeClass("active");
                        setTimeout(function(){
                            $productButtonText.addClass("active");
                        },300);
                        $productButtonPlaceholder.removeClass("active");
                    },2000);


                    setTimeout(function(){
                        $productButtonPlaceholder.hide();
                        $productButton.show();
                    },4000);


                    $productContainer.find(".js-added-to-cart-product-message").slideDown();


                    if (isQuickShop) {
                        closeModal($(".js-addtocart:not(.js-addtocart-placeholder)"));
                        if ($(window).width() < 768) {
                            cleanURLHash();
                        }
                    }


                    if ($(window).width() > 768) {
                        $(".js-toggle-cart").click();
                    }else{
                       {# Show notification and hide it only after second added to cart #}


                        setTimeout(function(){
                            $(".js-alert-added-to-cart").show().addClass("notification-visible").removeClass("notification-hidden");
                        },500);


                        if (typeof $.cookie('first_product_added_successfully') === 'undefined') {
                            $.cookie('first_product_added_successfully', true, { path: '/', expires: 7 }); 
                        } else{
                            setTimeout(function(){
                                $(".js-alert-added-to-cart").removeClass("notification-visible").addClass("notification-hidden");
                                setTimeout(function(){
                                    $('.js-cart-notification-item-img').attr('src', '');
                                    $(".js-alert-added-to-cart").hide();
                                },2000);
                            },8000);
                        }
                    }
                }
                var callback_error = function(){


                    {# Restore real button visibility in case of error #}


                    $productButtonPlaceholder.removeClass("active");
                    $productButtonText.fadeIn("active");
                    $productButtonAdding.removeClass("active");
                    $productButtonPlaceholder.hide();
                    $productButton.show();
                }
                $prod_form = $(this).closest("form");
                LS.addToCartEnhanced(
                    $prod_form,
                    '{{ "Agregar al carrito" | translate }}',
                    '{{ "Agregando..." | translate }}',
                    '{{ "¡Uy! No tenemos más stock de este producto para agregarlo al carrito." | translate }}',
                    {{ store.editable_ajax_cart_enabled ? 'true' : 'false' }},
                        callback_add_to_cart,
                        callback_error
                );
            {% endif %}
        }
    });

Configuraciones

En el archivo config/settings.txt vamos a agregar un checkbox para activar y desactivar la funcionalidad. Vamos a ubicarlo dentro de la sección Listado de productos.

    title
        title = Compra rápida
    checkbox
        name = quick_shop
        description = Permitir que tus clientes puedan agregar productos a su carrito rápidamente desde el listado de productos

Traducciones

En este paso agregamos los textos para las traducciones en el archivo config/translations.txt

es "Compra rápida"
pt "Compra rápida"
es_mx "Compra rápida"

es "Permitir que tus clientes puedan agregar productos a su carrito rápidamente desde el listado de productos"
pt "Permitir que seus clientes possam agregar produtos ao seu carrinho rapidamente na lista de produtos"
es_mx "Permitir que tus clientes puedan agregar productos a su carrito rápidamente desde el listado de productos"

es "Compra rápida de"
pt "Compra rápida de"
en "Quickshop of"
es_mx "Compra rápida de"

Activación

Por último podés activar el modal desde el Administrador nube, en la sección de Personalizar tu diseño actual dentro de las Listado de productos: