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();
}
}
}
Learn how to build powerful custom workflows in Shopify Flow with expert guidance from ...
By Jacqui May 7, 2025Did You Know? May is named after Maia, the Roman goddess of growth and flourishing! ...
By JasonH May 2, 2025Discover opportunities to improve SEO with new guidance available from Shopify’s growth...
By Jacqui May 1, 2025