Source: listeners/add-to-cart.js

// @vitest-environment happy-dom

import Listener from "./listener.js";
import Cart from "../cart.js";
import { createCartItem } from "../cart-item.js";

/**
 * 'ga4_add_to_cart' - triggered when the user clicks a '+' button on an item in the sidecart.
 * Uses the CartJS cart to query the item that was updated and generate the event payload.
 * @class
 * @extends Listener
 * @param {Object} cart - the CartJS cart object
 */
export class AddToCartButtonListener extends Listener {
    eventName = "ga4_add_to_cart";

    constructor(cart) {
        super();
        this.cart = new Cart(cart);
    }

    /**
     * Getter that returns an array of items that were added to the cart.
     * The array is empty if no items were added.
     * Subclasses can override this method to control the event payload.
     * @memberof AddToCartButtonListener
     * @type {Array}
     */
    get addedItems() {
        if (!this.link) return [];

        const newQuantity = parseInt(this.link.getAttribute('data-cart-quantity'), 10);
        const id = this.link.getAttribute('data-cart-update-id');
        const oldItem = this.cart.getItemByVariantID(id);

        return (newQuantity > oldItem.quantity)
            ? [{ ...oldItem, quantity: newQuantity }]
            : [];
    }

    listen() {
        const sidecart = document.getElementById('sidecart');
        sidecart.addEventListener('click', (({ target }) => {
            this.link = target.closest('a[data-cart-update-id]');
            this.triggerIfAdded();
        }));
    }

    triggerIfAdded() {
        if (this.addedItems.length > 0) {
            this.trigger(this.addedItems);
        }
    }

    createPayload(addedItems) {
        const currency = this.cart.currency;
        const items = [];
        let value = 0;

        for (const item of addedItems) {
            const quantity = Math.abs(this.getQuantityDifference(item));
            const cartItem = createCartItem(item, { quantity, currency });
            items.push(cartItem);
            value += Math.abs(cartItem.price * cartItem.quantity);
        }

        return {
            ecommerce: {
                value,
                currency,
                items
            }
        };
    }

    /**
     * Compares the added item against the existing cart and returns the difference in quantity.
     * @memberof AddToCartButtonListener
     * @param {Object} item - the item added
     * @param {string | number} item.id
     * @param {number} item.quantity
     * @returns {number} the quantity increase in the added item
     */
    getQuantityDifference(item) {
        const oldItem = this.cart.getItem(item);
        const oldQuantity = oldItem ? oldItem.quantity : 0;
        return item.quantity - oldQuantity;
    }
}

/**
 * 'ga4_add_to_cart' - triggered when the quantity of an item is increased on the /cart page.
 * Listens for the cart form submit event.
 * Uses the CartJS cart to query the item that was updated and generate the event payload.
 * Reuses the `createPayload` implementation of {@link AddToCartButtonListener}.
 * @class
 * @extends AddToCartButtonListener
 * @param {Object} cart - the CartJS cart object
 */
export class AddToCartFormListener extends AddToCartButtonListener {
    constructor(cart) {
        super(cart);
        this.form = document.querySelector('form.cart');
    }

    get addedItems() {
        const addedItems = [];
        const newQuantities = this.getNewQuantities();
        newQuantities.forEach((quantity, i) => {
            const oldItem = this.cart.items[i];
            const newItem = { ...oldItem, quantity };
            if (quantity > oldItem.quantity) addedItems.push(newItem);
        });
        return addedItems;
    }

    listen() {
        this.form.addEventListener('submit', () => this.triggerIfAdded());
    }

    /**
     * Gets the new quantity values for each item on the /cart page.
     * This is used to determine which items increased in quantity (ie, addedItems)
     * @memberof AddToCartFormListener
     * @returns {Array<number>} the quantity values from the cart form
     */
    getNewQuantities() {
        const formData = new FormData(this.form);
        const updates = formData.getAll("updates[]");
        return updates.map((value) => parseInt(value, 10));
    }
}

/**
 * 'ga4_add_to_cart' - triggered when a request to '/cart/add.js' resolves successfully.
 * Uses an XMLHttpRequest object to determine the item added to the cart.
 * Uses the CartJS cart to query the item that was updated and generate the event payload.
 * Reuses the `createPayload` implementation of {@link AddToCartButtonListener}.
 * @class
 * @extends AddToCartButtonListener
 * @param {Object} request - the XMLHttpRequest object
 * @param {Object} cart - the CartJS cart object
 */
export class AddToCartRequestListener extends AddToCartButtonListener {
    constructor(request, cart) {
        super(cart);
        this.request = request;
    }

    get addedItems() {
        const isAddRequest = this.request.responseURL.includes('/cart/add.js');
        if (!isAddRequest) return [];

        const responseBody = JSON.parse(this.request.responseText);

        // response body may either be an item or an object containing an array of items.
        return responseBody.items || [responseBody];
    }

    listen() {
        this.request.addEventListener('load', () => this.triggerIfAdded());
    }
}