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-item-image 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="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="js-item-image 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 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 %}" data-variation-id="{{ variation.id }}">
            {% embed "snipplets/forms/form-select.tpl" with{select_label: true, select_label_name: '' ~ variation.name ~ '', select_for: 'variation_' ~ loop.index , select_id: 'variation_' ~ loop.index, 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

⚠️ A partir del día 30 de enero de 2023, la librería jQuery será removida del código de nuestras tiendas, por lo tanto la función "$" no podrá ser utilizada.

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 #}

            jQueryNuvem("#quickshop-modal .js-item-product").removeClass("js-swiper-slide-visible js-item-slide");
            jQueryNuvem("#quickshop-modal .js-quickshop-container").attr( { 'data-variants' : '' , 'data-quickshop-id': '' } );
            jQueryNuvem("#quickshop-modal .js-item-product").attr('data-product-id', '');

            {# Wait for modal to become invisible before removing form #}
            
            setTimeout(function(){
                var $quickshop_form = jQueryNuvem("#quickshop-form").find('.js-product-form');
                var $item_form_container = jQueryNuvem(".js-quickshop-opened").find(".js-item-variants");
                
                $quickshop_form.detach().appendTo($item_form_container);
                jQueryNuvem(".js-quickshop-opened").removeClass("js-quickshop-opened");
            },350);
        };

    {% endif %}
    
    {# Full screen mobile modals back events #}

    if (window.innerWidth < 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 #}

        jQueryNuvem(document).on("click", ".js-fullscreen-modal-open", function(e) {
            e.preventDefault();
            var modal_url_hash = jQueryNuvem(this).data("modalUrl");
            window.location.hash = modal_url_hash;
        });

        {# Close full screen modal: Remove url hash #}

        jQueryNuvem(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(jQueryNuvem(".js-fullscreen-modal").hasClass("modal-show")){

                    {# Remove body lock only if a single modal is visible on screen #}

                    if(jQueryNuvem(".js-modal.modal-show").length == 1){
                        jQueryNuvem("body").removeClass("overflow-none");
                    }

                    var $opened_modal = jQueryNuvem(".js-fullscreen-modal.modal-show");
                    var $opened_modal_overlay = $opened_modal.prev();

                    $opened_modal.removeClass("modal-show");
                    setTimeout(() => $opened_modal.hide(), 500);
                    $opened_modal_overlay.fadeOut(500);

                    {% if settings.quick_shop %}
                        restoreQuickshopForm();
                    {% endif %}
                }
            }
        }
    }
    
    jQueryNuvem(document).on("click", ".js-modal-open", function(e) {
        e.preventDefault(); 
        var modal_id = jQueryNuvem(this).data('toggle');
        var $overlay_id = jQueryNuvem('.js-modal-overlay[data-modal-id="' + modal_id + '"]');
        if (jQueryNuvem(modal_id).hasClass("modal-show")) {
            let modal = jQueryNuvem(modal_id).removeClass("modal-show");
            setTimeout(() => modal.hide(), 500);
        } else {

            {# Lock body scroll if there is no modal visible on screen #}
            
            if(!jQueryNuvem(".js-modal.modal-show").length){
                jQueryNuvem("body").addClass("overflow-none");
            }
            $overlay_id.fadeIn(400);
            jQueryNuvem(modal_id).detach().appendTo("body");
            $overlay_id.detach().insertBefore(modal_id);
            jQueryNuvem(modal_id).show().addClass("modal-show");
        }             
    });

    jQueryNuvem(document).on("click", ".js-modal-close", function(e) {
        e.preventDefault();  
        {# Remove body lock only if a single modal is visible on screen #}

        if(jQueryNuvem(".js-modal.modal-show").length == 1){
            jQueryNuvem("body").removeClass("overflow-none");
        }
        var $modal = jQueryNuvem(this).closest(".js-modal");
        var modal_id = $modal.attr('id');
        var $overlay_id = jQueryNuvem('.js-modal-overlay[data-modal-id="#' + modal_id + '"]');
        $modal.removeClass("modal-show");
        setTimeout(() => $modal.hide(), 500);
        $overlay_id.fadeOut(500);
        {% if settings.quick_shop %}
            restoreQuickshopForm();
        {% endif %}

        {# Close full screen modal: Remove url hash #}

        if ((window.innerWidth < 768) && (jQueryNuvem(this).hasClass(".js-fullscreen-modal-close"))) {
            goBackBrowser();
        }    
    });

    jQueryNuvem(document).on("click", ".js-modal-overlay", function(e) {
        e.preventDefault();
        {# Remove body lock only if a single modal is visible on screen #}


        if(jQueryNuvem(".js-modal.modal-show").length == 1){
            jQueryNuvem("body").removeClass("overflow-none");
        }
        var modal_id = jQueryNuvem(this).data('modalId');
        let modal = jQueryNuvem(modal_id).removeClass("modal-show");
        setTimeout(() => modal.hide(), 500); 
        jQueryNuvem(this).fadeOut(500);   
        {% if settings.quick_shop %}
            restoreQuickshopForm();
        {% endif %}


        if (jQueryNuvem(this).hasClass("js-fullscreen-overlay") && (window.innerWidth < 768)) {
            cleanURLHash();
        }
    });

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

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

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

    {# If quickshop is used from modal, use quickshop-id from the item that opened it #}
    
    if($quickshop_parent_wrapper.hasClass("js-quickshop-modal")){
        var quick_id = jQueryNuvem(".js-quickshop-opened .js-quickshop-container").data("quickshopId");
    }else{
        var quick_id = $quickshop_parent_wrapper.data("quickshopId");
    }

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

        var $quickshop_parent = jQueryNuvem(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);

    } else {
        LS.changeVariant(changeVariant, '#single-product');
    }

    {# Offer and discount labels update #}

    var $this_product_container = jQueryNuvem(this).closest(".js-product-container");

    if($this_product_container.hasClass("js-quickshop-container")){
        var this_quickshop_id = $this_product_container.attr("data-quickshop-id");
        var $this_product_container = jQueryNuvem('.js-product-container[data-quickshop-id="'+this_quickshop_id+'"]');
    }
    var $this_compare_price = $this_product_container.find(".js-compare-price-display");
    var $this_price = $this_product_container.find(".js-price-display");
    var $installment_container = $this_product_container.find(".js-product-payments-container");
    var $installment_text = $this_product_container.find(".js-max-installments-container");
    var $this_add_to_cart = $this_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();

    // Calculate new discount percentage based on difference between filtered old and new prices
    const percentageDifference = window.moneyDifferenceCalculator.percentageDifferenceFromString(compare_price_value, price_value);
    if(percentageDifference){
        $this_product_container.find(".js-offer-percentage").text(percentageDifference);
        $this_product_container.find(".js-offer-label").css("display" , "table");
    }

    if ($this_compare_price.css("display") == "none" || !percentageDifference) {
        $this_product_container.find(".js-offer-label").hide();
    }

    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 #}

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

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

        var available_variant_color = jQueryNuvem(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(jQueryNuvem(this), otherOption, otherOptions[0]);
                    changeSelect(jQueryNuvem(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_attribute = element.closest('.js-item-product').find('.js-color-variant-available-' + (optionIndex + 1)).data('value');
            var selected_option = element.closest('.js-item-product').find('#' + selected_option_attribute + " option").filter(function(el) {
                return el.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 current_image = jQueryNuvem('.js-item-product[data-product-id="'+variant.product_id+'"] .js-item-image');
        current_image.attr('srcset', variant.image_url);
    });
    
{% endif %}

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

    {# Get width of the placeholder button #}
    var productButttonWidth = jQueryNuvem(".js-addtocart-placeholder-inline").prev(".js-addtocart").innerWidth();
    jQueryNuvem(".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:

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

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

    var $productContainer = jQueryNuvem(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");

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

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

    if (!isQuickShop) {
        if(jQueryNuvem(".js-product-slide-img.js-active-variant").length) {
            var imageSrc = $productContainer.find('.js-product-slide-img.js-active-variant').data('srcset').split(' ')[0];
        } else {
            var imageSrc = $productContainer.find('.js-product-slide-img').attr('srcset').split(' ')[0];
        }
        var name = $productContainer.find('.js-product-name').text();
        var price = $productContainer.find('.js-price-display').text();
    } else {
        var imageSrc = jQueryNuvem(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 (!jQueryNuvem(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");
        $productButtonText.removeClass("active");
        setTimeout(function(){
            $productButtonAdding.addClass("active");
        },300);

        {% if settings.ajax_cart %}

            var callback_add_to_cart = function(){

                {# Animate cart amount #}

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

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

                {# Fill notification info #}

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

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

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

                {# Set products amount wording visibility #}

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

                if(cartItemsAmount > 1){
                    jQueryNuvem(".js-cart-counts-plural").show();
                    jQueryNuvem(".js-cart-counts-singular").hide();
                }else{
                    jQueryNuvem(".js-cart-counts-singular").show();
                    jQueryNuvem(".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.css('display' , 'inline-block');
                },4000);

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

                if (isQuickShop) {
                    jQueryNuvem("#quickshop-modal").removeClass('modal-show');
                    jQueryNuvem(".js-modal-overlay[data-modal-id='#quickshop-modal']").hide();
                    jQueryNuvem("body").removeClass("overflow-none");
                    restoreQuickshopForm();
                    if (window.innerWidth < 768) {
                        cleanURLHash();
                    }
                }
                
               {# Show notification and hide it only after second added to cart #}

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

                if (!cookieService.get('first_product_added_successfully')) {
                    cookieService.set('first_product_added_successfully', 1, 7 ); 
                } else{
                    setTimeout(function(){
                        jQueryNuvem(".js-alert-added-to-cart").removeClass("notification-visible").addClass("notification-hidden");
                        setTimeout(function(){
                            jQueryNuvem('.js-cart-notification-item-img').attr('src', '');
                            jQueryNuvem(".js-alert-added-to-cart").hide();
                        },2000);
                    },8000);
                }

                {# Update shipping input zipcode on add to cart #}

                {# Use zipcode from input if user is in product page, or use zipcode cookie if is not #}

                if (jQueryNuvem("#product-shipping-container .js-shipping-input").val()) {
                    zipcode_on_addtocart = jQueryNuvem("#product-shipping-container .js-shipping-input").val();
                    jQueryNuvem("#cart-shipping-container .js-shipping-input").val(zipcode_on_addtocart);
                    jQueryNuvem(".js-shipping-calculator-current-zip").text(zipcode_on_addtocart);
                } else if (cookieService.get('calculator_zipcode')){
                    var zipcode_from_cookie = cookieService.get('calculator_zipcode');
                    jQueryNuvem('.js-shipping-input').val(zipcode_from_cookie);
                    jQueryNuvem(".js-shipping-calculator-current-zip").text(zipcode_from_cookie);
                }
            }
            var callback_error = function(){

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

                $productButtonPlaceholder.removeClass("active");
                $productButtonText.fadeIn("active");
                $productButtonAdding.removeClass("active");
                $productButtonPlaceholder.hide();
                $productButton.css('display' , 'inline-block');
            }
            $prod_form = jQueryNuvem(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: