Skip to content

Instantly share code, notes, and snippets.

@nilocortex
Created April 2, 2020 21:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nilocortex/2cff68b72358bd144748a2a1d817c8be to your computer and use it in GitHub Desktop.
Save nilocortex/2cff68b72358bd144748a2a1d817c8be to your computer and use it in GitHub Desktop.
Variant image patch - v1.4.8 - v1.4.10
{% comment %}
Set the extension of your color files below. Use 'png', 'jpeg', 'jpg' or 'gif'.
{% endcomment %}
{% assign swatchIndex = index %}
{% assign colPage = collPage %}
{% assign swatchTotal = total %}
{%- assign varGenProd = product -%}
{%- assign mainVariant = varGenProd.selected_or_first_available_variant -%}
{% assign file_extension = 'png' %}
{%- assign sharedSelectClasses = 'product-single__variants mobile-prod-padding' | append:' ' -%}
{%- assign singleOpSelectClasses = sharedSelectClasses | append:'single-variant-available' -%}
{%- assign multiOpSelectClasses = sharedSelectClasses | append:'multiop-select' -%}
{% if swatch == blank %}
<div class="swatch error">
<p>You must include the snippet swatch.liquid with the name of a product option.</p>
<p>Use: <code>{% raw %}{% include 'swatch' with 'name of your product option here' %}{% endraw %}</code></p>
<p>Example: <code>{% raw %}{% include 'swatch' with 'Color' %}{% endraw %}</code></p>
</div>
{% else %}
{% assign found_option = false %}
{% assign is_color = false %}
{% assign option_index = 0 %}
{% for option in product.options %}
{% if option == swatch %}
{% assign found_option = true %}
{% assign option_index = forloop.index0 %}
{% assign downcased_option = swatch | downcase %}
{% if downcased_option contains 'color' or downcased_option contains 'colour' %}
{% assign is_color = true %}
{% endif %}
{% endif %}
{% endfor %}
{% unless found_option %}
{% else %}
{%if colPage %}
{% assign colorCount = 0 %}
<div class="swatch clearfix" data-product-option="{{ swatch }}">
{% assign values = '' %}
{% for variant in product.variants %}
{% assign value = variant.options[option_index] %}
{% unless values contains value %}
{% assign values = values | join: '||' %}
{% assign values = values | append: '||' | append: value %}
{% assign values = values | split: '||' %}
<div class="swatch-element color {% if variant.available %}available{% else %}soldout{% endif %}">
{% if is_color %}
<div class="tooltip">{{ value }}</div>
{% endif %}
<div id="swatch-{{ option_index }}-{{ value | handle }}-{{section.id}}" {% unless variant.available %}disabled{% endunless %} {% unless variant.available %}data-unavailable="true"{% endunless %} class="colSwatch"/>
{% if is_color %}
{% assign colorCount = colorCount | plus:1 %}
<label for="swatch-{{ option_index }}-{{ value | handle }}-{{section.id}}" {% unless variant.available %} class="disabled-swatch" {%endunless%} style="background-color: {{ value | split: ' ' | last | handle }}; background-image: url({{ value | handle | append: '.' | append: file_extension | asset_url }})">
<img class="crossed-out" src="{{ 'soldout.png' | asset_url }}" />
</label>
</div>
{% endif %}
</div>
{% endunless %}
<!--
{% if variant.available %}
<script>
$(function(){
jQuery('.swatch[data-option-index="{{ option_index }}"] .{{ value | handle }}').removeClass('soldout').addClass('available').find('div.colSwatch').removeAttr('disabled');
});
</script>
{% endif %}
//-->
{%if colorCount >= 21%}
{% break %}
{%endif%}
{% endfor %}
</div>
{%break%}
{%endif%}
{% if swatchTotal == 1 %}
<div class="swatch clearfix" name="id" id="productSelect" class="{{ singleOpSelectClasses }}" data-prod-base-price="{{ varGenProd.price }}" data-option-index="{{ option_index }}" data-product-option="{{ swatch }}">
<div class="header">{{ swatch }}</div>
{% assign values = '' %}
{% for variant in product.variants %}
{% assign value = variant.options[option_index] %}
{% unless values contains value %}
{% assign values = values | join: '||' %}
{% assign values = values | append: '||' | append: value %}
{% assign values = values | split: '||' %}
<div class="swatch-element {% if is_color %}color {% endif %}{{ value | handle }} {% if variant.available %}available{% else %}soldout{% endif %}" >
{% if is_color %}
<div class="tooltip">{{ value }}</div>
{% endif %}
<input id="swatch-{{ option_index }}-{{ value | handle }}-{{section.id}}" type="radio" name="id" {% if mainVariant == variant%} checked{% endif %} {% unless variant.available %}disabled{% endunless %} data-sku="{{ variant.sku }}" value="{{ variant.id }}" data-base-price="{{variant.price }}" {% unless variant.compare_at_price == blank %}data-compare-price="{{ variant.compare_at_price }}"{%endunless%} {% unless variant.available %}data-unavailable="true"{% endunless %} data-img-id="{{ variant.featured_media.id }}" data-title="{{variant.title}}"/>
{% if is_color %}
<label for="swatch-{{ option_index }}-{{ value | handle }}-{{section.id}}" {% unless variant.available %} class="disabled-swatch" {%endunless%} style="background-color: {{ value | split: ' ' | last | handle }}; background-image: url({{ value | handle | append: '.' | append: file_extension | asset_url }})">
<img class="crossed-out" src="{{ 'soldout.png' | asset_url }}" />
</label>
{% else %}
<label for="swatch-{{ option_index }}-{{ value | handle }}-{{section.id}}" {% unless variant.available %} class="disabled-swatch" {%endunless%}>
{{ value }}
<img class="crossed-out" src="{{ 'soldout.png' | asset_url }}" />
</label>
{% endif %}
</div>
{% endunless %}
<!--
{% if variant.available %}
<script>
$(function(){
jQuery('.swatch[data-option-index="{{ option_index }}"] .{{ value | handle }}').removeClass('soldout').addClass('available').find(':radio').removeAttr('disabled');
});
</script>
{% endif %}
//-->
{% endfor %}
</div>
{%else%}
{%comment%}
For multiple options to tie into JS
{%endcomment%}
<div class="swatch clearfix multiop-select" id="productSelect-op{{ swatchIndex }}" data-prod-base-price="{{ varGenProd.price }}" data-option-index="{{ option_index }}" data-opnum="{{ swatchIndex }}" data-product-option="{{ swatch }}">
<div class="header">{{ swatch }}</div>
{% assign values = '' %}
{% for variant in product.variants %}
{% assign value = variant.options[option_index] %}
{% unless values contains value %}
{% assign values = values | join: '||' %}
{% assign values = values | append: '||' | append: value %}
{% assign values = values | split: '||' %}
<div data-value="{{ value | escape }}" data-img-id="{{ variant.featured_media.id }}" class="swatch-element {% if is_color %}color {% endif %}{{ value | handle }} available {% if mainVariant.options[option_index] == variant.options[option_index] %}selected{% endif %}">
{% if is_color %}
<div class="tooltip">{{ value }}</div>
{% endif %}
<input id="swatch-{{ option_index }}-{{ value | handle }}-{{section.id}}" value="{{ value | escape }}" type="radio" name="option-{{ option_index }}" {% if mainVariant.options[option_index] == variant.options[option_index] %} checked{% endif %} data-img-id="{{ variant.featured_media.id }}" data-title="{{variant.title}}" />
{% if is_color %}
<label for="swatch-{{ option_index }}-{{ value | handle }}-{{section.id}}" style="background-color: {{ value | split: ' ' | last | handle }}; background-image: url({{ value | handle | append: '.' | append: file_extension | asset_url }})">
<img class="crossed-out" src="{{ 'soldout.png' | asset_url }}" />
</label>
{% else %}
<label for="swatch-{{ option_index }}-{{ value | handle }}-{{section.id}}">
{{ value }}
<img class="crossed-out" src="{{ 'soldout.png' | asset_url }}" />
</label>
{% endif %}
</div>
{% endunless %}
{% endfor %}
</div>
{%endif%}
{% endunless %}
{% endif %}
theme.Variants = (function () {
var productVariants = {};
return {
/***** Public Properties *****/
// Custom event types. NOTE: Events are tied to the
// section's container (selector: [data-section-id])
eventStrs: {
onQtyChanged: 'theme:variants:qtyChanged',
onVariantChanged: 'theme:variants:changed'
},
/***** Private Properties *****/
//Selectors used for jQuery targeting
_selectors: {
//Variant Selection
multiOpRealProdSelect: '#actualProdSelect',
multiOpAllSelects: '.multiop-select',
multiOpSelect: '#productSelect-op',
prodSelect: '#productSelect',
smart: '.shopify-payment-button',
//Quantity Controls
qtyContainer: '#quantity-container',
qtyField: '#quantity',
qtyMinus: '#quantity-minus',
qtyPlus: '#quantity-plus'
},
/***** Public Methods *****/
/*
=function init(container)
Initializes the select events for the variation dropdowns.
Automatically determines whether to initialize a multi-option
select or single-option based on id based selectors.
=Params
(mixed) container: Value can be a jQuery selector string, jQuery
object, or element. The value should return the
container that may have variant select(s) when
passed to jQuery.
(bool) initControls: (optional) If true, the quantity controls will
be initialized.
*/
init: function (container, initControls) {
var self = this;
var $container = $(container);
// Check to see if the hidden select exists for multi-option. If it does,
// init the multi-option selects. Otherwise, init single-option.
var $select = $container.find(self._selectors.multiOpRealProdSelect);
if ($select.length > 0) {
self._initMultiOpSelect($container, $container.find(self._determineAllSelectorFromStr(self._selectors.multiOpAllSelects)));
} else {
self._initSingleOpSelect($container, $container.find(self._selectors.prodSelect));
}
if (initControls) {
self.initQtyControls($container);
}
},
uninit: function (container) {
productVariants = [];
},
/*
=function mapProductVariant(productID, productOptionsString, variantID, variantTitle, variantOptionsString, variantPrice, variantCompareAtPrice, variantImageID, variantSKU, variantAvailable)
Builds up a map of variants for a specific product
=Params
(string) productID
(string) productOptionsString: string of options delimited by "||"
(string) variantID
(string) variantTitle
(string) variantOptionsString: string of option values delimited by "||"
(number) variantPrice
(number) variantCompareAtPrice
(number) variantImageID
(string) variantSKU
(bool) variantAvailable: true if the variant is available for purchase, or false if it not. For a variant to be available, its variant.inventory_quantity must be greater than zero or variant.inventory_policy must be set to continue. A variant with no variant.inventory_management is also considered available.
*/
mapProductVariant: function (productID, productOptionsString, variantID, variantTitle, variantOptionsString, variantPrice, variantCompareAtPrice, variantImageID, variantSKU, variantAvailable) {
if (!productVariants[productID]) productVariants[productID] = {};
productVariants[productID][variantID] = {
id: variantID,
title: variantTitle,
price: variantPrice,
compareAtPrice: variantCompareAtPrice,
sku: variantSKU,
imageId: variantImageID,
available: variantAvailable,
options: {}
};
var productOptions = productOptionsString.split("||");
var variantOptions = variantOptionsString.split("||");
productOptions.forEach(function (productOption, optionIndex) {
productVariants[productID][variantID].options[productOption] = variantOptions[optionIndex] || null;
});
theme.Utilities.debug_log('mapProductVariant', productVariants);
},
/*
=function initQtyControls(container)
Initializes the events that control quantity changes
=Params
(mixed) container: Value can be a jQuery selector string, jQuery
object, or element. The value should return the
container that may have the quantity controls
when passed to jQuery
*/
initQtyControls: function (container) {
var self = this;
var $container = $(container);
//Init the plus button click event
$container.find(this._selectors.qtyPlus).click(function (e) {
e.preventDefault();
self._adjustQty($(self._selectors.qtyField, $(this).closest(self._selectors.qtyContainer)), 1);
});
//Init the minus button click event
$container.find(this._selectors.qtyMinus).click(function (e) {
e.preventDefault();
self._adjustQty($(self._selectors.qtyField, $(this).closest(self._selectors.qtyContainer)), -1);
});
//Init the change event for the quantity number field
$container.find(this._selectors.qtyField).change(function (e) {
e.preventDefault();
self._adjustQty($(this), 0);
});
},
/*
=function getSelectedVariant(container)
Convenience function that will return the currently selected variant
within the provided container. Automatically determines whether to
check against a multi-option or single-option select. If neither are valid,
it will return whatever input has name="id".
=Params
(mixed) container: Value can be a jQuery selector string, jQuery
object, or element. The value should return the
container(s) that may have variant select(s) when
passed to jQuery
*/
getSelectedVariant: function (container) {
// Check to see if the hidden select exists for multi-option. If it does,
// return it's current selection
var $select = $(this._selectors.multiOpRealProdSelect, container);
if ($select.length > 0) {
if ($select.find('option').length > 0) {
return $select.find('option:selected');
} else {
return $select.find('input:checked');
}
} else {
// If it doesn't, return the single-option's selection
$select = $(this._selectors.prodSelect);
if ($select.length > 0) {
if ($select.find('option').length > 0) {
return $select.find('option:selected');
} else {
return $select.find('input:checked');
}
} else {
// If neither exist, return the default input used in forms
return $('input[name="id"]');
}
}
},
/*
=function getVariantQuantity(container)
Gets the current quantity from the quantity field. Returns 1 if
none can be found.
=Params
(mixed) container: Value can be a jQuery selector string, jQuery
object, or element. The value should return the
container(s) that may have variant select(s) when
passed to jQuery
*/
getVariantQuantity: function (container) {
var $qtyField = $(this._selectors.qtyField, container);
if ($qtyField.length > 0) {
return parseInt($qtyField.val());
} else {
return 1;
}
},
/*
=function gatherQuantityFields(container)
Returns all the quantity fields found within the container
=Params
(mixed) container: Value can be a jQuery selector string, jQuery
object, or element. The value should return the
container(s) that may have variant select(s) when
passed to jQuery
*/
gatherQuantityFields: function (container) {
return $(this._determineAllSelectorFromStr(this._selectors.qtyField), container);
},
/*
=function enableControl($control)
Enables the passed in control
=Params
(jQuery) $control: The control to enable
*/
enableControl: function ($control) {
$control.prop('disabled', false);
},
/*
=function disableControl($control)
Disables the passed in control
=Params
(jQuery) $control: The control to disable
*/
disableControl: function ($control) {
$control.prop('disabled', true);
},
/*
=function enableAllControls()
Enables ALL quantity controls on the page
*/
enableAllControls: function () {
var self = this;
var selector = self._determineAllSelectorFromStr(this._selectors.qtyContainer);
$(selector).each(function () {
var $this = $(this);
self._performOnQtyControls($this, self.enableControl);
});
},
/*
=function disableAllControls()
Disables ALL quantity controls on the page
*/
disableAllControls: function () {
var self = this;
var selector = self._determineAllSelectorFromStr(this._selectors.qtyContainer);
$(selector).each(function () {
var $this = $(this);
self._performOnQtyControls($this, self.disableControl);
});
},
/***** Private Methods *****/
/*
=function _variantForOptions(productID, options)
Returns a variant that represents the selected option values. If a variant can't be found, returns null
If an option value is null, it will be ignored in the comparison algo
=Params
(string) productID
(object) options
*/
_variantForOptions: function (productID, options) {
var foundVariant = null;
if (!!productVariants[productID]) {
$.each(productVariants[productID], function (variantID, variantData) {
var matches = true;
$.each(options, function (option, optionValue) {
matches = matches && ((optionValue === null) || (optionValue === variantData.options[option]));
});
if (matches) {
foundVariant = variantData;
return false;
}
});
}
return foundVariant;
},
/*
=function _initSingleOpSelect($select)
Initializes the single-option variant select
=Params
(jQuery) $select: The single-option select
*/
_initSingleOpSelect: function ($container, $select) {
var self = this;
$select.change(function (evt) {
var $this = $(this);
if ($this.find('option').length > 0) {
self._fireChangeEvent($container, self._getBaseVariantObj($this.find('option:selected')));
} else {
self._fireChangeEvent($container, self._getBaseVariantObj($this.find('input:checked')));
}
});
},
/*
=function _selectedOptions($container)
Get an object that represents the currently selected options ; if an option is not yet selected, its value will be "null"
=Params
(jQuery) $container: The container housing the select
(bool) $allowIncomplete: if true, an object is always returned: options not yet selected will be null'ed. If false, the function will return null if at least one option is not yet selected
*/
_selectedOptions: function ($container, $allowIncomplete) {
var selectedOptions = {};
$container.find("[data-product-option]").each(function () {
var $optionGroup = $(this);
var $productOption = $optionGroup.data("product-option");
var $selection = $optionGroup.find("option:selected, input:checked");
if ($selection.length === 0) {
// this option is not yet selected
if ($allowIncomplete) {
selectedOptions[$productOption] = null;
} else {
selectedOptions = null;
return false;
}
} else {
selectedOptions[$productOption] = $selection.val();
}
});
return selectedOptions;
},
/*
=function _variantUI($container, $select)
Manages the UI when an option is selected: hide unavailable combos, and disabled sold out ones
=Params
(jQuery) $container: The container housing the select
(jQuery) $select: The item selected
*/
_variantUI: function ($container, $select) {
var $variantGroup = $container.find('.variant-group');
var $productID = $variantGroup.data('product-id');
var $swatch = $variantGroup.data('swatch');
if (!$swatch) return;
theme.Utilities.debug_trace();
/*
User just clicked a specific option for a product... For all other options on the product,
we hide unavailable combos, and disabled sold out ones
IF for any option we touch there was a selection that is no longer available,
we replace the user's selection by the first available, not sold-out option
*/
var self = this;
var activatedOptionGroup = $select.closest("[data-product-option]");
var activatedProductOption = activatedOptionGroup.data("product-option");
var $sectionID = $container.closest('div[data-section-type="product"]').data('section-id');
var $productScript = $("#ProductJson-" + $sectionID);
var $allowIncomplete = true;
if($productScript.length > 0){
var $productData = JSON.parse($productScript.html())
$productData['variants'].forEach(function(item,i){
if(item.featured_media){
var fImage = item.featured_media.id
}else{
var fImage = null
}
var prodOpt = '';
var varOpt = '';
$.each($productData.options, function(i,item){
if(prodOpt == ''){
prodOpt = item;
}else{
prodOpt = prodOpt + "||" + item;
}
});
$.each(item.options, function(i,item){
if(varOpt == ''){
varOpt = item;
}else{
varOpt = varOpt + "||" + item;
}
});
self.mapProductVariant(""+$productData.id+"",prodOpt.toString(),""+item.id+"",""+item.title+"",varOpt.toString(),item.price,item.compare_at_price,fImage,""+item.sku+"",item.available)
})
}
var $selectOptionGroupInput = function (input, productOption, options, optionGroup, ignoreProductOptions) {
var $option = $(input);
var $optionContainer = $option.closest(".swatch-element");
options[productOption] = $option.val();
var variant = self._variantForOptions($productID, options);
if (!variant) { // this combo of options is not available -- hide the option
$optionContainer.hide();
} else if (!variant.available) { // combo exists, but no longer available
$optionContainer.removeClass("available").addClass("soldout").show();
$optionContainer.find("input").prop("disabled", true);
$optionContainer.find("label").addClass("disabled-swatch");
} else { // all good - this option is part of a valid combo
$optionContainer.find("input").prop("disabled", false);
$optionContainer.find("label").removeClass("disabled-swatch");
$optionContainer.addClass("available").removeClass("soldout").css('display', 'inline-block');
}
if (!variant || !variant.available) { // if the option is unavailable, we need to select an alternative
// First, make sure the unavailable option is unchecked properly
$optionContainer.removeClass("selected");
$option.prop("checked", false);
// Then, find a suitable fallback option and select it
if(!optionGroup.find(".swatch-element.available input:checked").length) {
// build a list of option values we already tried...
if(!ignoreProductOptions) ignoreProductOptions = [];
ignoreProductOptions.push(options[productOption]);
// ... and ignore them when looking for an alternate option value to pick
var ignoreSelector = ignoreProductOptions.join("']):not([data-value='");
if(ignoreSelector) ignoreSelector = ":not([data-value='" + ignoreSelector + "'])";
var $fallbackOptionContainer = optionGroup.find(".swatch-element.available" + ignoreSelector).first()
if(!!$fallbackOptionContainer.length) {
var $fallbackOption = $fallbackOptionContainer.find("input").first()
if(!!$fallbackOption.length) {
$fallbackOption.prop("checked", true);
$selectOptionGroupInput($fallbackOption, productOption, options, optionGroup, ignoreProductOptions);
}
} else {
// we could not build a valid combo with the next option's value so pick the very first combo that matches the activatedProductOption
optionGroup.closest(".variant-group").find("[data-product-option='"+activatedProductOption+"']")
var $variantGroup = optionGroup.closest(".variant-group")
// uncheck all other selected options
$variantGroup.find("[data-product-option]:not([data-product-option='" + activatedProductOption + "']):not([data-product-option='" + productOption + "'])").each(function() {
var $swatchToReset = $(this);
$swatchToReset.find(".selected").removeClass("selected");
$swatchToReset.find("input:checked").prop("checked", false);
options[$swatchToReset.data("product-option")] = null;
});
if(!!ignoreProductOptions.length) {
// then restart the cascade of finding the right option for the current productOption
$fallbackOptionContainer = optionGroup.find(".swatch-element.available[data-value='" + ignoreProductOptions[0] + "']").first();
if(!!$fallbackOptionContainer.length) {
$fallbackOption = $fallbackOptionContainer.find("input").first()
if(!!$fallbackOption.length) {
$fallbackOption.prop("checked", true);
$selectOptionGroupInput($fallbackOption, productOption, options, optionGroup, []);
}
}
}
}
}
}
};
$container.find("[data-product-option]").each(function (index) {
var $optionGroup = $(this);
var $productOption = $optionGroup.data("product-option");
if (($productOption !== activatedProductOption) && !!index) {
var options = self._selectedOptions($container, $allowIncomplete); // the base options object
Object.keys(options).forEach(option => {
if((option !== $productOption) && (option !== activatedProductOption)) options[option] = null;
});
// for each possible option value, see if a variant exists and is available...
$optionGroup.find("input").each(function () {
$selectOptionGroupInput(this, $productOption, options, $optionGroup);
});
}
});
},
/*
=function _selectVariant($container)
Selects the corresponding variant, based on all selected product options ;
Also manages the UI: hide unavailable combos, and disabled sold out ones
=Params
(jQuery) $container: The container housing the select
*/
_selectVariant: function ($container) {
var self = this;
var $variantGroup = $container.find('.variant-group');
var $productID = $variantGroup.data('product-id');
//var $swatch = $variantGroup.data('swatch');
var selectedOptions = self._selectedOptions($container);
var selectedVariant = null;
if (selectedOptions) {
// all options selected: get the equivalent variant
selectedVariant = self._variantForOptions($productID, selectedOptions);
if (selectedVariant) {
var $actualSelector = $container.find(self._selectors.multiOpRealProdSelect);
$actualSelector.val(selectedVariant.id);
}
}
self._fireChangeEvent($container, selectedVariant);
},
/*
=function _initMultiOpSelect($container, $select)
Initializes the single-option variant select
=Params
(jQuery) $container: The container housing the select
(jQuery) $select: The multi-option select
*/
_initMultiOpSelect: function ($container, $select) {
var self = this;
$select.change(function () {
self._variantUI($container, $(this));
self._selectVariant($container);
});
self._variantUI($container, $container.find(".swatch-element input:checked").first());
},
/*
=function _adjustQty($qtyField, offset)
Adjusts the quantity by offset in the $qtyField and keeps it
within at or above the minimum
=Params
(jQuery) $qtyField: The input that is storing the quantity
(int) offset: The amount to adjust the quantity by
*/
_adjustQty: function ($qtyInput, offset) {
var currentVal = parseInt($qtyInput.val());
var min = $qtyInput.attr('min');
if (isNaN(currentVal)) {
//If the value is somehow invalid, set it to the min
$qtyInput.val(min);
this._fireQtyChangeEvent(min);
} else {
//Add the offset and if it it below the minimum, set to min
currentVal += offset;
if (currentVal < min) {
currentVal = min;
}
//Update the number input with the new quantity
$qtyInput.val(currentVal);
this._fireQtyChangeEvent($qtyInput.mySection(), currentVal);
}
},
/*
=function _determineAllSelectorFromStr(str)
If we need to select ALL of a selector, and if it is an ID selector,
we cannot simply pass it in as jQuery will return only the first
object found. This function will alter the selector to return
all elements with the same ID. If it is not an ID, it returns str
=Params
(string) str: The selector to check
*/
_determineAllSelectorFromStr: function (str) {
if (str[0] == '#') {
return '[id="' + str.substring(1) + '"]';
} else {
return str;
}
},
/*
=function _performOnQtyControls($container, func)
Will find the 3 quantity controls within the container and
then pass them into the passed in function "func"
=Params
(jQuery) $container: The container that has the quantity controls
(function) func: The function to pass the controls into
*/
_performOnQtyControls: function ($container, func) {
func($container.find(this._selectors.qtyMinus));
func($container.find(this._selectors.qtyField))
func($container.find(this._selectors.qtyPlus))
},
/*
=function _getBaseVariantObj($selection)
Creates and returns an object containing the base information on
the passed in selection. Object is used in variant change events
=Params
(jQuery) $selection: The selected variant
*/
_getBaseVariantObj: function ($selection) {
return {
available: !$selection.data('unavailable'),
compareAtPrice: $selection.data('compare-price'),
id: $selection.val(),
imageId: $selection.data('img-id'),
price: $selection.data('base-price'),
sku: $selection.data('sku'),
title: $selection.data('title')
};
},
/*
=function _fireChangeEvent($container, variantObj)
Fires the custom event on the element's section for variant select change.
=Params
(jQuery) $container: The container to trigger the event on
(obj) variantObj: Object containing the selected variant's data to
be passed to the event
*/
_fireChangeEvent: function ($container, variantObj) {
$container.trigger(this.eventStrs.onVariantChanged, [variantObj]);
},
/*
=function _fireQtyChangeEvent(newQty)
Fires the custom event on the element's section for quantity change.
=Params
(jQuery) $container: The container to trigger the event on
(int) newQty: The new quantity
*/
_fireQtyChangeEvent: function ($container, newQty) {
$container.trigger(this.eventStrs.onQtyChanged, [newQty]);
}
};
}());
timber.productPage = function (options) {
var moneyFormat = options.money_format,
variant = options.variant,
selector = options.selector,
section_id = $('.ProductSection').data('section-id');
// Selectors
var $productImage = $('#ProductPhotoImg'),
$addToCart = $('#AddToCart'),
$addToCartDesk = $('#AddToCartDesk'),
$AddToCartSold = $('#AddToCartSold'),
$smart = $('.shopify-payment-button'),
$productPrice = $('#ProductPrice, #ProductPriceMobile'),
$comparePrice = $('#ComparePrice'),
$quantityElements = $('.quantity-selector, label + .js-qty'),
$addToCartText = $('#AddToCartText'),
$addToCartTextDesk = $('#AddToCartTextDesk');
if (variant) {
// Update variant image, if one is set
if (variant.featured_image) {
var prodImg = ".ProductImg-" + $('.product-template__container').data('section-id');
var v_img = $(prodImg + '[data-image-id="' + variant.featured_media.id + '"]');
var newPosition = v_img.closest('data-slick-index').attr('data-slick-index');
if (newPosition !== undefined) {
$('.product__slides').slick('slickGoTo', newPosition);
}
}
var $this = $(this);
var $option_selected = $this.find('option:selected');
if ($option_selected.data('img-id')) {
var img_id = $this.find('option:selected').data('img-id');
var $prod_slider = $details_container.find('.featured-product-slider');
var newPosition = $prod_slider.find('img[data-fimg-id="' + img_id + '"]').closest('data-slick-index').attr('data-slick-index');
if (newPosition !== undefined) {
$prod_slider.slick('slickGoTo', newPosition);
}
}
// Select a valid variant if available
if (variant.available) {
// Available, enable the submit button, change text, show quantity elements
$addToCart.removeClass('disabled').prop('disabled', false);
$smart.show();
$addToCartDesk.show();
$AddToCartSold.hide();
$addToCartText.html({{ 'products.product.add_to_cart' | t | json }});
$quantityElements.show();
} else {
// Sold out, disable the submit button, change text, hide quantity elements
$smart.addClass('disabled').prop('disabled', true).hide();
$addToCart.addClass('disabled').prop('disabled', true);
$addToCartDesk.hide();
$addToCartDesk.addClass('disabled').prop('disabled', true);;
$AddToCartSold.show();
$AddToCartSold.addClass('disabled').prop('disabled', true);
$addToCartText.html({{ 'products.product.sold_out' | t | json }});
$addToCartTextDesk.html({{ 'products.product.sold_out' | t | json }});
$quantityElements.hide();
}
// Regardless of stock, update the product price
$productPrice.html(Shopify.formatMoney(variant.price, moneyFormat));
// Also update and show the product's compare price if necessary
if (variant.compare_at_price > variant.price) {
$comparePrice
.html({{ 'products.product.compare_at' | t | json }} + ' ' + Shopify.formatMoney(variant.compare_at_price, moneyFormat))
.show();
} else {
$comparePrice.hide();
}
} else {
// The variant doesn't exist, disable submit button.
// This may be an error or notice that a specific variant is not available.
// To only show available variants, implement linked product options:
// - http://docs.shopify.com/manual/configuration/store-customization/advanced-navigation/linked-product-options
$addToCart.addClass('disabled').prop('disabled', true);
$addToCartText.html({{ 'products.product.unavailable' | t | json }});
$quantityElements.hide();
}
};
theme.Variants = (function () {
var productVariants = {};
return {
/***** Public Properties *****/
// Custom event types. NOTE: Events are tied to the
// section's container (selector: [data-section-id])
eventStrs: {
onQtyChanged: 'theme:variants:qtyChanged',
onVariantChanged: 'theme:variants:changed'
},
/***** Private Properties *****/
//Selectors used for jQuery targeting
_selectors: {
//Variant Selection
multiOpRealProdSelect: '#actualProdSelect',
multiOpAllSelects: '.multiop-select',
multiOpSelect: '#productSelect-op',
prodSelect: '#productSelect',
smart: '.shopify-payment-button',
//Quantity Controls
qtyContainer: '#quantity-container',
qtyField: '#quantity',
qtyMinus: '#quantity-minus',
qtyPlus: '#quantity-plus'
},
/***** Public Methods *****/
/*
=function init(container)
Initializes the select events for the variation dropdowns.
Automatically determines whether to initialize a multi-option
select or single-option based on id based selectors.
=Params
(mixed) container: Value can be a jQuery selector string, jQuery
object, or element. The value should return the
container that may have variant select(s) when
passed to jQuery.
(bool) initControls: (optional) If true, the quantity controls will
be initialized.
*/
init: function (container, initControls) {
var self = this;
var $container = $(container);
// Check to see if the hidden select exists for multi-option. If it does,
// init the multi-option selects. Otherwise, init single-option.
var $select = $container.find(self._selectors.multiOpRealProdSelect);
if ($select.length > 0) {
self._initMultiOpSelect($container, $container.find(self._determineAllSelectorFromStr(self._selectors.multiOpAllSelects)));
} else {
self._initSingleOpSelect($container, $container.find(self._selectors.prodSelect));
}
if (initControls) {
self.initQtyControls($container);
}
},
uninit: function (container) {
productVariants = [];
},
/*
=function mapProductVariant(productID, productOptionsString, variantID, variantTitle, variantOptionsString, variantPrice, variantCompareAtPrice, variantImageID, variantSKU, variantAvailable)
Builds up a map of variants for a specific product
=Params
(string) productID
(string) productOptionsString: string of options delimited by "||"
(string) variantID
(string) variantTitle
(string) variantOptionsString: string of option values delimited by "||"
(number) variantPrice
(number) variantCompareAtPrice
(number) variantImageID
(string) variantSKU
(bool) variantAvailable: true if the variant is available for purchase, or false if it not. For a variant to be available, its variant.inventory_quantity must be greater than zero or variant.inventory_policy must be set to continue. A variant with no variant.inventory_management is also considered available.
*/
mapProductVariant: function (productID, productOptionsString, variantID, variantTitle, variantOptionsString, variantPrice, variantCompareAtPrice, variantImageID, variantSKU, variantAvailable) {
if (!productVariants[productID]) productVariants[productID] = {};
productVariants[productID][variantID] = {
id: variantID,
title: variantTitle,
price: variantPrice,
compareAtPrice: variantCompareAtPrice,
sku: variantSKU,
imageId: variantImageID,
available: variantAvailable,
options: {}
};
var productOptions = productOptionsString.split("||");
var variantOptions = variantOptionsString.split("||");
productOptions.forEach(function (productOption, optionIndex) {
productVariants[productID][variantID].options[productOption] = variantOptions[optionIndex] || null;
});
theme.Utilities.debug_log('mapProductVariant', productVariants);
},
/*
=function initQtyControls(container)
Initializes the events that control quantity changes
=Params
(mixed) container: Value can be a jQuery selector string, jQuery
object, or element. The value should return the
container that may have the quantity controls
when passed to jQuery
*/
initQtyControls: function (container) {
var self = this;
var $container = $(container);
//Init the plus button click event
$container.find(this._selectors.qtyPlus).click(function (e) {
e.preventDefault();
self._adjustQty($(self._selectors.qtyField, $(this).closest(self._selectors.qtyContainer)), 1);
});
//Init the minus button click event
$container.find(this._selectors.qtyMinus).click(function (e) {
e.preventDefault();
self._adjustQty($(self._selectors.qtyField, $(this).closest(self._selectors.qtyContainer)), -1);
});
//Init the change event for the quantity number field
$container.find(this._selectors.qtyField).change(function (e) {
e.preventDefault();
self._adjustQty($(this), 0);
});
},
/*
=function getSelectedVariant(container)
Convenience function that will return the currently selected variant
within the provided container. Automatically determines whether to
check against a multi-option or single-option select. If neither are valid,
it will return whatever input has name="id".
=Params
(mixed) container: Value can be a jQuery selector string, jQuery
object, or element. The value should return the
container(s) that may have variant select(s) when
passed to jQuery
*/
getSelectedVariant: function (container) {
// Check to see if the hidden select exists for multi-option. If it does,
// return it's current selection
var $select = $(this._selectors.multiOpRealProdSelect, container);
if ($select.length > 0) {
if ($select.find('option').length > 0) {
return $select.find('option:selected');
} else {
return $select.find('input:checked');
}
} else {
// If it doesn't, return the single-option's selection
$select = $(this._selectors.prodSelect);
if ($select.length > 0) {
if ($select.find('option').length > 0) {
return $select.find('option:selected');
} else {
return $select.find('input:checked');
}
} else {
// If neither exist, return the default input used in forms
return $('input[name="id"]');
}
}
},
/*
=function getVariantQuantity(container)
Gets the current quantity from the quantity field. Returns 1 if
none can be found.
=Params
(mixed) container: Value can be a jQuery selector string, jQuery
object, or element. The value should return the
container(s) that may have variant select(s) when
passed to jQuery
*/
getVariantQuantity: function (container) {
var $qtyField = $(this._selectors.qtyField, container);
if ($qtyField.length > 0) {
return parseInt($qtyField.val());
} else {
return 1;
}
},
/*
=function gatherQuantityFields(container)
Returns all the quantity fields found within the container
=Params
(mixed) container: Value can be a jQuery selector string, jQuery
object, or element. The value should return the
container(s) that may have variant select(s) when
passed to jQuery
*/
gatherQuantityFields: function (container) {
return $(this._determineAllSelectorFromStr(this._selectors.qtyField), container);
},
/*
=function enableControl($control)
Enables the passed in control
=Params
(jQuery) $control: The control to enable
*/
enableControl: function ($control) {
$control.prop('disabled', false);
},
/*
=function disableControl($control)
Disables the passed in control
=Params
(jQuery) $control: The control to disable
*/
disableControl: function ($control) {
$control.prop('disabled', true);
},
/*
=function enableAllControls()
Enables ALL quantity controls on the page
*/
enableAllControls: function () {
var self = this;
var selector = self._determineAllSelectorFromStr(this._selectors.qtyContainer);
$(selector).each(function () {
var $this = $(this);
self._performOnQtyControls($this, self.enableControl);
});
},
/*
=function disableAllControls()
Disables ALL quantity controls on the page
*/
disableAllControls: function () {
var self = this;
var selector = self._determineAllSelectorFromStr(this._selectors.qtyContainer);
$(selector).each(function () {
var $this = $(this);
self._performOnQtyControls($this, self.disableControl);
});
},
/***** Private Methods *****/
/*
=function _variantForOptions(productID, options)
Returns a variant that represents the selected option values. If a variant can't be found, returns null
If an option value is null, it will be ignored in the comparison algo
=Params
(string) productID
(object) options
*/
_variantForOptions: function (productID, options) {
var foundVariant = null;
if (!!productVariants[productID]) {
$.each(productVariants[productID], function (variantID, variantData) {
var matches = true;
$.each(options, function (option, optionValue) {
matches = matches && ((optionValue === null) || (optionValue === variantData.options[option]));
});
if (matches) {
foundVariant = variantData;
return false;
}
});
}
return foundVariant;
},
/*
=function _initSingleOpSelect($select)
Initializes the single-option variant select
=Params
(jQuery) $select: The single-option select
*/
_initSingleOpSelect: function ($container, $select) {
var self = this;
$select.change(function (evt) {
var $this = $(this);
if ($this.find('option').length > 0) {
self._fireChangeEvent($container, self._getBaseVariantObj($this.find('option:selected')));
} else {
self._fireChangeEvent($container, self._getBaseVariantObj($this.find('input:checked')));
}
});
},
/*
=function _selectedOptions($container)
Get an object that represents the currently selected options ; if an option is not yet selected, its value will be "null"
=Params
(jQuery) $container: The container housing the select
(bool) $allowIncomplete: if true, an object is always returned: options not yet selected will be null'ed. If false, the function will return null if at least one option is not yet selected
*/
_selectedOptions: function ($container, $allowIncomplete) {
var selectedOptions = {};
$container.find("[data-product-option]").each(function () {
var $optionGroup = $(this);
var $productOption = $optionGroup.data("product-option");
var $selection = $optionGroup.find("option:selected, input:checked");
if ($selection.length === 0) {
// this option is not yet selected
if ($allowIncomplete) {
selectedOptions[$productOption] = null;
} else {
selectedOptions = null;
return false;
}
} else {
selectedOptions[$productOption] = $selection.val();
}
});
return selectedOptions;
},
/*
=function _variantUI($container, $select)
Manages the UI when an option is selected: hide unavailable combos, and disabled sold out ones
=Params
(jQuery) $container: The container housing the select
(jQuery) $select: The item selected
*/
_variantUI: function ($container, $select) {
var $variantGroup = $container.find('.variant-group');
var $productID = $variantGroup.data('product-id');
var $swatch = $variantGroup.data('swatch');
if (!$swatch) return;
theme.Utilities.debug_trace();
/*
User just clicked a specific option for a product... For all other options on the product,
we hide unavailable combos, and disabled sold out ones
IF for any option we touch there was a selection that is no longer available,
we replace the user's selection by the first available, not sold-out option
*/
var self = this;
var activatedOptionGroup = $select.closest("[data-product-option]");
var activatedProductOption = activatedOptionGroup.data("product-option");
var $sectionID = $container.closest('div[data-section-type="product"]').data('section-id');
var $productScript = $("#ProductJson-" + $sectionID);
var $allowIncomplete = true;
if($productScript.length > 0){
var $productData = JSON.parse($productScript.html())
$productData['variants'].forEach(function(item,i){
if(item.featured_media){
var fImage = item.featured_media.id
}else{
var fImage = null
}
var prodOpt = '';
var varOpt = '';
$.each($productData.options, function(i,item){
if(prodOpt == ''){
prodOpt = item;
}else{
prodOpt = prodOpt + "||" + item;
}
});
$.each(item.options, function(i,item){
if(varOpt == ''){
varOpt = item;
}else{
varOpt = varOpt + "||" + item;
}
});
self.mapProductVariant(""+$productData.id+"",prodOpt.toString(),""+item.id+"",""+item.title+"",varOpt.toString(),item.price,item.compare_at_price,fImage,""+item.sku+"",item.available)
})
}
var $selectOptionGroupInput = function (input, productOption, options, optionGroup, ignoreProductOptions) {
var $option = $(input);
var $optionContainer = $option.closest(".swatch-element");
options[productOption] = $option.val();
var variant = self._variantForOptions($productID, options);
if (!variant) { // this combo of options is not available -- hide the option
$optionContainer.hide();
} else if (!variant.available) { // combo exists, but no longer available
$optionContainer.removeClass("available").addClass("soldout").show();
$optionContainer.find("input").prop("disabled", true);
$optionContainer.find("label").addClass("disabled-swatch");
} else { // all good - this option is part of a valid combo
$optionContainer.find("input").prop("disabled", false);
$optionContainer.find("label").removeClass("disabled-swatch");
$optionContainer.addClass("available").removeClass("soldout").css('display', 'inline-block');
}
if (!variant || !variant.available) { // if the option is unavailable, we need to select an alternative
// First, make sure the unavailable option is unchecked properly
$optionContainer.removeClass("selected");
$option.prop("checked", false);
// Then, find a suitable fallback option and select it
if(!optionGroup.find(".swatch-element.available input:checked").length) {
// build a list of option values we already tried...
if(!ignoreProductOptions) ignoreProductOptions = [];
ignoreProductOptions.push(options[productOption]);
// ... and ignore them when looking for an alternate option value to pick
var ignoreSelector = ignoreProductOptions.join("']):not([data-value='");
if(ignoreSelector) ignoreSelector = ":not([data-value='" + ignoreSelector + "'])";
var $fallbackOptionContainer = optionGroup.find(".swatch-element.available" + ignoreSelector).first()
if(!!$fallbackOptionContainer.length) {
var $fallbackOption = $fallbackOptionContainer.find("input").first()
if(!!$fallbackOption.length) {
$fallbackOption.prop("checked", true);
$selectOptionGroupInput($fallbackOption, productOption, options, optionGroup, ignoreProductOptions);
}
} else {
// we could not build a valid combo with the next option's value so pick the very first combo that matches the activatedProductOption
optionGroup.closest(".variant-group").find("[data-product-option='"+activatedProductOption+"']")
var $variantGroup = optionGroup.closest(".variant-group")
// uncheck all other selected options
$variantGroup.find("[data-product-option]:not([data-product-option='" + activatedProductOption + "']):not([data-product-option='" + productOption + "'])").each(function() {
var $swatchToReset = $(this);
$swatchToReset.find(".selected").removeClass("selected");
$swatchToReset.find("input:checked").prop("checked", false);
options[$swatchToReset.data("product-option")] = null;
});
if(!!ignoreProductOptions.length) {
// then restart the cascade of finding the right option for the current productOption
$fallbackOptionContainer = optionGroup.find(".swatch-element.available[data-value='" + ignoreProductOptions[0] + "']").first();
if(!!$fallbackOptionContainer.length) {
$fallbackOption = $fallbackOptionContainer.find("input").first()
if(!!$fallbackOption.length) {
$fallbackOption.prop("checked", true);
$selectOptionGroupInput($fallbackOption, productOption, options, optionGroup, []);
}
}
}
}
}
}
};
$container.find("[data-product-option]").each(function (index) {
var $optionGroup = $(this);
var $productOption = $optionGroup.data("product-option");
if (($productOption !== activatedProductOption) && !!index) {
var options = self._selectedOptions($container, $allowIncomplete); // the base options object
Object.keys(options).forEach(option => {
if((option !== $productOption) && (option !== activatedProductOption)) options[option] = null;
});
// for each possible option value, see if a variant exists and is available...
$optionGroup.find("input").each(function () {
$selectOptionGroupInput(this, $productOption, options, $optionGroup);
});
}
});
},
/*
=function _selectVariant($container)
Selects the corresponding variant, based on all selected product options ;
Also manages the UI: hide unavailable combos, and disabled sold out ones
=Params
(jQuery) $container: The container housing the select
*/
_selectVariant: function ($container) {
var self = this;
var $variantGroup = $container.find('.variant-group');
var $productID = $variantGroup.data('product-id');
//var $swatch = $variantGroup.data('swatch');
var selectedOptions = self._selectedOptions($container);
var selectedVariant = null;
if (selectedOptions) {
// all options selected: get the equivalent variant
selectedVariant = self._variantForOptions($productID, selectedOptions);
if (selectedVariant) {
var $actualSelector = $container.find(self._selectors.multiOpRealProdSelect);
$actualSelector.val(selectedVariant.id);
}
}
self._fireChangeEvent($container, selectedVariant);
},
/*
=function _initMultiOpSelect($container, $select)
Initializes the single-option variant select
=Params
(jQuery) $container: The container housing the select
(jQuery) $select: The multi-option select
*/
_initMultiOpSelect: function ($container, $select) {
var self = this;
$select.change(function () {
self._variantUI($container, $(this));
self._selectVariant($container);
});
self._variantUI($container, $container.find(".swatch-element input:checked").first());
},
/*
=function _adjustQty($qtyField, offset)
Adjusts the quantity by offset in the $qtyField and keeps it
within at or above the minimum
=Params
(jQuery) $qtyField: The input that is storing the quantity
(int) offset: The amount to adjust the quantity by
*/
_adjustQty: function ($qtyInput, offset) {
var currentVal = parseInt($qtyInput.val());
var min = $qtyInput.attr('min');
if (isNaN(currentVal)) {
//If the value is somehow invalid, set it to the min
$qtyInput.val(min);
this._fireQtyChangeEvent(min);
} else {
//Add the offset and if it it below the minimum, set to min
currentVal += offset;
if (currentVal < min) {
currentVal = min;
}
//Update the number input with the new quantity
$qtyInput.val(currentVal);
this._fireQtyChangeEvent($qtyInput.mySection(), currentVal);
}
},
/*
=function _determineAllSelectorFromStr(str)
If we need to select ALL of a selector, and if it is an ID selector,
we cannot simply pass it in as jQuery will return only the first
object found. This function will alter the selector to return
all elements with the same ID. If it is not an ID, it returns str
=Params
(string) str: The selector to check
*/
_determineAllSelectorFromStr: function (str) {
if (str[0] == '#') {
return '[id="' + str.substring(1) + '"]';
} else {
return str;
}
},
/*
=function _performOnQtyControls($container, func)
Will find the 3 quantity controls within the container and
then pass them into the passed in function "func"
=Params
(jQuery) $container: The container that has the quantity controls
(function) func: The function to pass the controls into
*/
_performOnQtyControls: function ($container, func) {
func($container.find(this._selectors.qtyMinus));
func($container.find(this._selectors.qtyField))
func($container.find(this._selectors.qtyPlus))
},
/*
=function _getBaseVariantObj($selection)
Creates and returns an object containing the base information on
the passed in selection. Object is used in variant change events
=Params
(jQuery) $selection: The selected variant
*/
_getBaseVariantObj: function ($selection) {
return {
available: !$selection.data('unavailable'),
compareAtPrice: $selection.data('compare-price'),
id: $selection.val(),
imageId: $selection.data('img-id'),
price: $selection.data('base-price'),
sku: $selection.data('sku'),
title: $selection.data('title')
};
},
/*
=function _fireChangeEvent($container, variantObj)
Fires the custom event on the element's section for variant select change.
=Params
(jQuery) $container: The container to trigger the event on
(obj) variantObj: Object containing the selected variant's data to
be passed to the event
*/
_fireChangeEvent: function ($container, variantObj) {
$container.trigger(this.eventStrs.onVariantChanged, [variantObj]);
},
/*
=function _fireQtyChangeEvent(newQty)
Fires the custom event on the element's section for quantity change.
=Params
(jQuery) $container: The container to trigger the event on
(int) newQty: The new quantity
*/
_fireQtyChangeEvent: function ($container, newQty) {
$container.trigger(this.eventStrs.onQtyChanged, [newQty]);
}
};
}());
{% comment %}
/snippets/variant-selects.liquid
Creates the appropriate select(s) for this product's variants. If the product only has 1
variant, a hidden input is generated instead. The hidden variant will contain all items
below unless stated otherwise.
Each select will contain:
-the base price (in cents) of the product (value of product.price)
Each option will contain the variant's:
-SKU
-base price (in cents)
-compare at price (in cents), if available
-availability
-featured image's ID (NOT included in the hidden input)
NOTE: the id attribute is used by the theme's JavaScript, specifically in theme.Variants.
Do not adjust these attributes unless you also adjust it in the JavaScript code.
{% endcomment %}
{% comment %} Classes used for the selects {% endcomment %}
{%- assign sharedSelectClasses = 'product-single__variants mobile-prod-padding' | append:' ' -%}
{%- assign singleOpSelectClasses = sharedSelectClasses | append:'single-variant-available' -%}
{%- assign multiOpSelectClasses = sharedSelectClasses | append:'multiop-select' -%}
{%- assign showSwatch = settings.use_swatches -%}
{% comment %} Section variables {% endcomment %}
{%- assign varGenProd = product -%}
{%- assign mainVariant = varGenProd.selected_or_first_available_variant -%}
{% comment %}
"variant-group" groups all variant (options) selectors ;
we store a map of variants in a javascript object, to be used by the swatches UI code
we map the variant object's index-based option arguments into associative attributes (i.e. "option1" becomes "color", ...)
{% endcomment %}
<script data-variant-data>
{%- for variant in varGenProd.variants -%}
{%- capture vItem -%}"{{ varGenProd.id }}", "{{ product.options | join:'||' }}", "{{ variant.id }}", '{{ variant.title }}', '{{ variant.options | join:"||" }}', {{ variant.price }}, {{ variant.compare_at_price | plus:0 }}, {{ variant.featured_media.id | plus:0 }}, "{{ variant.sku }}", {{ variant.available }}{%- endcapture -%}
theme.Variants.mapProductVariant({{vItem}});
{%- endfor -%}
</script>
<div class="variant-group" data-product-id="{{ varGenProd.id }}" data-swatch="{{showSwatch}}">
{% if varGenProd.variants.size > 1 %}
{% if varGenProd.options.size == 1 %}
{% if showSwatch %}
{% if product.available and product.variants.size > 1 %}
{% for option in product.options %}
{% include 'swatch' with option, total:1 %}
{%endfor%}
{% endif %}
{%else%}
{% comment %} Generate a select for a product with multiple variants, but only 1 option {% endcomment %}
{% assign showDrop = false%}
{% for variant in varGenProd.variants %}
{% if variant.available %}
{% assign showDrop = true%}
{%endif%}
{%endfor%}
{%if showDrop%}
<label for="productSelect">{{ varGenProd.options.first | escape }}</label>
<select name="id" id="productSelect" class="{{ singleOpSelectClasses }}" data-prod-base-price="{{ varGenProd.price }}">
{% assign status = false%}
{% for variant in varGenProd.variants %}
{% if variant.available %}
{% assign status = true%}
{{mainVariant}}
{{variant}}
<option {% if variant == mainVariant %}selected="selected"{% endif %} data-sku="{{ variant.sku }}" value="{{ variant.id }}" data-base-price="{{variant.price }}" {% unless variant.compare_at_price == blank %}data-compare-price="{{ variant.compare_at_price }}"{%endunless%} {% unless variant.available %}data-unavailable="true"{% endunless %} data-img-id="{{ variant.featured_media.id }}" data-title="{{variant.title}}">
{{ variant.title }}
</option>
{% else %}
<option disabled="disabled">
{{ variant.title }} - {{ 'products.product.sold_out' | t }}
</option>
{% endif %}
{% endfor %}
{% unless status%}
<option>Sold out</option>
{% endunless%}
</select>
{%endif%}
{%endif%}
{% else %}
{% comment %} Generate selects for a product with multiple options {% endcomment %}
{% if showSwatch %}
{% if product.available and product.variants.size > 1 %}
{% for option in product.options %}
<div class="variant-group-multiple">
{% include 'swatch' with option, index: forloop.index0, total: forloop.length %}
</div>
{% endfor %}
{% endif %}
{%else%}
{% for option in varGenProd.options_with_values %}
<div class="variant-group-multiple" data-product-option="{{ option.name }}">
<label for="productSelect-op{{ forloop.index0 }}">{{ option.name }}</label>
<select id="productSelect-op{{ forloop.index0 }}" class="{{ multiOpSelectClasses }}" data-opnum="{{ forloop.index0 }}">
{% for optionVal in option.values %}
<option value='{{ optionVal }}' {% if mainVariant.option1 == optionVal or mainVariant.option2 == optionVal or mainVariant.option3 == optionVal %}selected{% endif %}>{{ optionVal }}</option>
{% endfor %}
</select>
</div>
{% endfor %}
{%endif%}
<select name="id" id="actualProdSelect" data-prod-base-price="{{ product.price }}" style="display:none">
{% for variant in varGenProd.variants %}
{% comment %} variantOps (option[data-opid]) is a special string to ID the option based on the selected options {% endcomment %}
{%- assign variantOps = variant.option1 -%}
{%- assign variantOps = variantOps | append:' / ' | append:variant.option2 -%}
{% unless variant.option3 == blank %}{%- assign variantOps = variantOps | append:' / ' | append:variant.option3 -%}{% endunless %}
<option {% if variant == mainVariant %}selected="selected"{% endif %} data-sku="{{ variant.sku }}" value="{{ variant.id }}" data-base-price="{{ variant.price }}" {% unless variant.compare_at_price == blank %}data-compare-price="{{ variant.compare_at_price }}"{%endunless%} data-opid="{{ variantOps }}" data-img-id="{{ variant.featured_media.id }}" {% unless variant.available %}data-unavailable="true"{% endunless %} data-title='{{variant.title}}'>
{{ variant.title }}
</option>
{% endfor %}
</select>
{% endif %}
{% else %}
{% comment %} There is only a single variant, so generate a simple, hidden input for it {% endcomment %}
<input type="hidden" name="id" data-sku="{{ mainVariant.sku }}" data-base-price="{{ mainVariant.price }}" {% unless mainVariant.compare_at_price == blank %}data-compare-price="{{ mainVariant.compare_at_price }}"{% endunless %} value="{{ mainVariant.id }}" data-title="{{mainVariant.title}}"/>
{% endif %}
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment