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();
}
}
}
Starting a B2B store is a big undertaking that requires careful planning and execution. W...
By JasonH Sep 23, 2024By investing 30 minutes of your time, you can unlock the potential for increased sales,...
By Jacqui Sep 11, 2024We appreciate the diverse ways you participate in and engage with the Shopify Communi...
By JasonH Sep 9, 2024