Discuss and resolve questions on Liquid, JavaScript, themes, sales channels, and site speed enhancements.
class ProductGridItem {
constructor(options) {
this.el = options.el;
this.$el = jquery_default()(options.el);
this.id = options.id;
this.disableActionsToggle = 'disableActionsToggle' in options ? options.disableActionsToggle : false;
this.productQuickshop = null;
this.quickshopInitialVariant = null;
this.quickBuySettings = null;
this.actionsOpen = false;
this.defaultView = options.grid_list;
this.events = new EventHandler/* default */.Z();
if (options.lazy) {
this.lazyLoader = new LazyLoader_LazyLoader(this.$el[0], () => this._init());
} else {
this._init();
}
}
_init() {
this.$window = jquery_default()(window);
this.$html = jquery_default()('html');
this.content = this.el.querySelector('[data-product-item-content]');
this.actions = this.el.querySelector('[data-product-actions]');
this.swatchesEl = this.el.querySelector('[data-swatches]');
this.quickBuyEl = this.el.querySelector('[data-quick-buy]');
this.quickshopSlimEl = this.el.querySelector('[data-quickshop-slim]');
this.quickshopFullEl = this.el.querySelector('[data-quickshop-full]');
this.compareCheckbox = this.el.querySelector('[data-compare-checkbox]');
this.compareItem = this.el.querySelector('[data-compare-item]');
this.compareItemWrapper = this.el.querySelector('[data-compare-item-wrapper]');
this.hasProductActions = this.actions !== null;
this._addToCart = this._addToCart.bind(this);
this._actionsToggle = this._actionsToggle.bind(this);
this._openQuickShop = this._openQuickShop.bind(this);
if (this.hasProductActions) {
this._setSortByQueryParameters();
if (!this.disableActionsToggle && this.$html.hasClass('no-touch') && this.defaultView !== 'list-view') {
this.events.register(this.el, 'mouseenter', e => this._actionsToggle(e));
this.events.register(this.el, 'mouseleave', e => this._actionsToggle(e));
this.events.register(this.el, 'focusin', e => this._actionsToggle(e));
} // $scripts checks existence of script in header before attempting to inject
script_default()(jquery_default()('[data-scripts]').data('shopify-api-url'), () => {
this.events.register(this.quickBuyEl, 'click', e => this._addToCart(e));
this.events.register(this.quickshopSlimEl, 'click', e => this._openQuickShop(e));
this.events.register(this.quickshopFullEl, 'click', e => this._openQuickShop(e));
});
}
this.expandAnimation = transition({
el: this.content,
state: 'closed'
});
if (this.compareCheckbox) {
this.compareData = JSON.parse(this.el.querySelector('[data-product-compare-data]').innerHTML);
this.expandCheckboxAnimation = transition({
el: this.compareItemWrapper,
state: 'closed'
});
this.compareItemCheckbox = new Checkbox(this.compareItem);
this.events.register(this.compareCheckbox, 'change', () => this._updateProductCompare(this.compareCheckbox.checked));
const onUpdate = (_ref) => {
let {
atProductLimit
} = _ref;
if (components_ProductCompare.includes(this.compareData.handle)) {
this.compareCheckbox.disabled = false;
this.compareItemCheckbox.disabled = false;
this.compareItem.classList.add('productitem__compare--enabled');
this.compareItem.classList.remove('productitem__compare--disabled');
this.compareCheckbox.checked = true;
this.compareItemCheckbox.check();
} else {
this.compareCheckbox.checked = false;
this.compareItemCheckbox.uncheck();
this.compareCheckbox.disabled = atProductLimit;
this.compareItemCheckbox.disabled = atProductLimit;
this.compareItem.classList.toggle('productitem__compare--enabled', !atProductLimit);
this.compareItem.classList.toggle('productitem__compare--disabled', atProductLimit);
}
};
onUpdate({
atProductLimit: components_ProductCompare.atProductLimit
});
components_ProductCompare.runOnUpdate(onUpdate);
const onEnableChange = enabled => {
if (enabled) {
this._showCompareCheckbox();
} else if (!this.actionsOpen) {
this._hideCompareCheckbox();
}
};
onEnableChange(components_ProductCompare.enabled);
components_ProductCompare.addRunOnEnableChange(onEnableChange);
}
if (this.quickbuyEl !== null) {
this._initQuickBuy();
}
this._objectFitPolyfill();
if (this.swatchesEl) {
this.swatches = new GridItemSwatches({
el: this.$el[0],
setInitialVariant: id => {
this.quickshopInitialVariant = id;
},
product: this.product
});
}
}
_updateProductCompare(checkedForCompare) {
const {
handle,
title,
image,
imageAspectRatio,
url
} = this.compareData; // Ignore onboarding content
if (!handle) return;
if (checkedForCompare) {
components_ProductCompare.enable();
components_ProductCompare.add({
handle,
data: {
title,
image,
imageAspectRatio,
url
}
});
} else {
components_ProductCompare.remove(handle);
}
}
/**
* Make Shopify aware of releavent collection search info
* - tag
* - vendor
* - pagination
* - sorting criteria
*
* @private
*/
_setSortByQueryParameters() {
Shopify.queryParams = {};
if (location.search.length) {
for (let i = 0, aCouples = location.search.substr(1).split('&'); i < aCouples.length; i++) {
const aKeyValue = aCouples[i].split('='); // Reset the page number when we apply (i.e. don't add it to params)
if (aKeyValue.length > 1 && aKeyValue[0] !== 'page') {
Shopify.queryParams[decodeURIComponent(aKeyValue[0])] = aKeyValue[1];
}
}
}
}
_initQuickBuy() {
try {
this.quickBuySettings = JSON.parse(this.$el.find('[data-quick-buy-settings]').text());
} catch (error) {
console.warn(`Quick buy: invalid QuickBuy data found. ${error.message}`);
}
}
_openQuickShop(event) {
event.preventDefault();
const leftThumbsClass = event.currentTarget.hasAttribute('data-thumbs-left') ? ' quickshop-thumbs-left' : '';
const modalClass = event.currentTarget.hasAttribute('data-quickshop-full') ? `quickshop-full${leftThumbsClass}` : 'quickshop-slim';
if (this.productQuickshop) {
this.productQuickshop.unload();
}
this.productQuickshop = new ProductQuickshop({
$el: this.$el,
id: this.id,
modalClass,
trigger: this.$el.find('.productitem--title a'),
initialVariant: this.quickshopInitialVariant
});
}
_isObjectFitAvailable() {
return 'objectFit' in document.documentElement.style;
}
_objectFitPolyfill() {
if (this._isObjectFitAvailable()) {
return;
}
const $figure = jquery_default()('[data-product-item-image]', this.$el);
const featuredsrc=jquery_default()('img:not(.productitem--image-alternate)', $figure).attr('src');
const alternatesrc=jquery_default()('.productitem--image-alternate', $figure).attr('src');
$figure.addClass('product-item-image-no-objectfit');
$figure.css('background-image', `url("${featuredSrc}")`);
if (alternateSrc) {
this.events.register(this.el, 'mouseover', () => {
$figure.css('background-image', `url("${alternateSrc}")`);
});
this.events.register(this.el, 'mouseleave', () => {
$figure.css('background-image', `url("${featuredSrc}")`);
});
}
}
/**
* Get height of element, and combined height of element + actions
*
* @returns {{heightBase, heightExpanded: *}}
* @private
*/
_getHeights() {
const {
height
} = this.el.getBoundingClientRect();
const actionsHeight = this.actions.getBoundingClientRect().height;
return {
heightBase: height,
heightExpanded: height + actionsHeight
};
}
_actionsToggle(event) {
if (!Layout.isGreaterThanBreakpoint('M')) return;
const $currentTarget = jquery_default()(event.currentTarget);
const $target = jquery_default()(event.target);
let openProductItem = false; // This function gets called on the element as well as the document focusin, so we want to
// be extra careful that we are inside the product card in question. We want the product card
// to close if another product card has received focus.
const productHasFocus = this.$el.is($currentTarget) || this.$el.is($target) || this.$el.is($target.parents('.productgrid--item').first()) || event.type === 'focusin' && $target[0].contains(this.$el[0]);
if (event.type === 'mouseenter' || event.type === 'mouseleave') {
openProductItem = event.type === 'mouseenter';
} else if (productHasFocus) {
openProductItem = true;
}
if (openProductItem) {
this._showActions();
} else {
this._hideActions();
}
}
_showActions() {
if (this.actionsOpen) {
return;
}
const {
heightBase,
heightExpanded
} = this._getHeights();
this._showCompareCheckbox().then(compareHeight => {
this.el.style.setProperty('--base-height', `${heightBase}px`);
this.el.style.setProperty('--open-height', `${heightExpanded + compareHeight}px`); // Store set the outer grid item to be open so it knows to adjust its z-index
this.el.setAttribute('data-open', ''); // Start animation, and transition base height to expanded height (in CSS)
this.expandAnimation.animateTo('open');
this.focusinEvent = this.events.register(document, 'focusin', e => this._actionsToggle(e));
this.actionsOpen = true;
});
}
_hideActions() {
this.expandAnimation.animateTo('closed').then(() => {
this.el.style.removeProperty('--base-height');
this.el.removeAttribute('data-open');
});
this._hideCompareCheckbox();
if (this.focusinEvent) {
this.events.unregister(this.focusinEvent);
}
this.actionsOpen = false;
}
_showCompareCheckbox() {
if (!this.expandCheckboxAnimation || this.expandCheckboxAnimation.state === 'open') {
// Checkbox doesn't exist or is already visible and included in card height
return Promise.resolve(0);
}
return new Promise(resolve => {
this.expandCheckboxAnimation.animateTo('open', {
onStart: (_ref2) => {
let {
el
} = _ref2;
const {
scrollHeight
} = el.querySelector('[data-compare-item]');
this.el.style.setProperty('--compare-height', `${scrollHeight}px`);
resolve(scrollHeight);
}
});
});
}
_hideCompareCheckbox() {
if (!this.expandCheckboxAnimation) return; // Start animation and transition checkbox height
if (!components_ProductCompare.enabled) {
this.expandCheckboxAnimation.animateTo('closed').then(() => {
this.el.style.setProperty('--compare-height', '0px');
});
}
}
_addToCart(event) {
event.preventDefault();
if (this.addToCartFlyout) {
this.addToCartFlyout.unload();
}
const atcButton = event.currentTarget;
const variantID = atcButton.getAttribute('data-variant-id');
const formData = [{
name: 'id',
value: variantID
}, {
name: 'quantity',
value: 1
}];
const options = {
atcButton,
settings: {
moneyFormat: this.quickBuySettings.money_format,
cartRedirection: this.quickBuySettings.cart_redirection
}
};
this.addToCartFlyout = new AddToCartFlyout(formData, options);
}
unload() {
this.events.unregisterAll();
if (this.productQuickshop) {
this.productQuickshop.unload();
}
document.removeEventListener('focusin', this._actionsToggle);
if (this.swatches) {
this.swatches.unload();
}
if (this.lazyLoader) {
this.lazyLoader.unload();
}
}
}
Hey Community 👋 Did you know that March 15th is National Everything You Think Is W...
By JasonH Apr 1, 2025Discover how to increase the efficiency of commerce operations with Shopify Academy's l...
By Jacqui Mar 26, 2025Shopify and our financial partners regularly review and update verification requiremen...
By Jacqui Mar 14, 2025