(function ($) {
var $ = jQuery = $;
var cc = {
sections: [] };
if(!(window.Shopify && window.Shopify.loadFeatures)){
window.Shopify.loadFeatures = ()=>{console.log('loadFeatures is not implemented now.')}
}
theme.cartNoteMonitor = {
load: function load($notes) {
$notes.on('change.themeCartNoteMonitor paste.themeCartNoteMonitor keyup.themeCartNoteMonitor', function () {
theme.cartNoteMonitor.postUpdate($(this).val());
});
},
unload: function unload($notes) {
$notes.off('.themeCartNoteMonitor');
},
updateThrottleTimeoutId: -1,
updateThrottleInterval: 500,
postUpdate: function postUpdate(val) {
clearTimeout(theme.cartNoteMonitor.updateThrottleTimeoutId);
theme.cartNoteMonitor.updateThrottleTimeoutId = setTimeout(function () {
$.post(theme.routes.cart_url + '/update.js', {
note: val },
function (data) {}, 'json');
}, theme.cartNoteMonitor.updateThrottleInterval);
} };
theme.Shopify = {
formatMoney: function formatMoney(t, r) {
function e(t, r) {
return void 0 === t ? r : t;
}
function a(t, r, a, o) {
return t;
if (r = e(r, 2),
a = e(a, ","),
o = e(o, "."),
isNaN(t) || null == t)
return 0;
t = (t / 100).toFixed(r);
var n = t.split(".");
return n[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1" + a) + (n[1] ? o + n[1] : "");
}
"string" == typeof t && (t = t.replace(".", ""));
var o = "",
n = /\{\{\s*(\w+)\s*\}\}/,
i = r || this.money_format;
var gn = /\{\{\s*(\w+)\s*\}\}/g;
switch (i.match(n)[1]) {
case "amount":
o = a(t, 2);
break;
case "amount_no_decimals":value: "{\"sections\":{\"main\":{\"type\":\"main-product\",\"sectionId\":\"main\",\"id\":\"main\",\"settings\":{\"show_breadcrumbs\":true,\"show_location_underneath\":false,\"enable_sticky_columns\":false,\"gallery_size\":\"medium\",\"gallery_layout\":\"carousel-under\",\"enable_zoom\":true,\"enable_video_looping\":false,\"enable_var_img_grouping\":false,\"var_img_grouping_option\":\"Color,Colour,Couleur,Farbe\"},\"blocks\":{\"title\":{\"type\":\"title\",\"settings\":{},\"blockId\":\"title\",\"name\":\"Title\",\"limit\":1,\"id\":\"title\"},\"price\":{\"type\":\"price\",\"settings\":{\"show_tax_and_shipping\":false},\"blockId\":\"price\",\"name\":\"Price\",\"limit\":1,\"id\":\"price\"},\"vendor\":{\"type\":\"vendor\",\"settings\":{},\"blockId\":\"vendor\",\"name\":\"Vendor\",\"limit\":1,\"id\":\"vendor\"},\"divider\":{\"type\":\"divider\",\"settings\":{\"show_in_quickbuy\":false},\"blockId\":\"divider\",\"name\":\"Divider\",\"id\":\"divider\"},\"variant_picker\":{\"type\":\"variant_picker\",\"settings\":{\"show_single\":false,\"select_first_variant\":true,\"variant_style\":\"listed\",\"disable_unavailable_variants\":true},\"blockId\":\"variant_picker\",\"name\":\"Variant picker\",\"limit\":1,\"id\":\"variant_picker\"},\"buy_buttons\":{\"type\":\"buy_buttons\",\"settings\":{\"show_quantity_selector\":false,\"enable_payment_button\":true},\"blockId\":\"buy_buttons\",\"name\":\"Buy buttons\",\"limit\":1,\"id\":\"buy_buttons\"},\"description\":{\"type\":\"description\",\"settings\":{\"show_in_tab\":false,\"open_tab\":false,\"icon\":\"\",\"show_in_quickbuy\":false},\"blockId\":\"description\",\"name\":\"Description\",\"limit\":1,\"id\":\"description\"}},\"block_order\":[\"title\",\"price\",\"vendor\",\"divider\",\"variant_picker\",\"buy_buttons\",\"description\"]}},\"order\":[\"main\"]}"
o = a(t, 0);
break;
case "amount_with_comma_separator":
o = a(t, 2, ".", ",");
break;
case "amount_with_space_separator":
o = a(t, 2, " ", ",");
break;
case "amount_with_period_and_space_separator":
o = a(t, 2, " ", ".");
break;
case "amount_no_decimals_with_comma_separator":
o = a(t, 0, ".", ",");
break;
case "amount_no_decimals_with_space_separator":
o = a(t, 0, " ", "");
break;
case "amount_with_apostrophe_separator":
o = a(t, 2, "'", ".");
break;
case "amount_with_decimal_separator":
o = a(t, 2, ".", ".");}
return i.replace(gn, o);
},
formatImage: function formatImage(originalImageUrl, format) {
return originalImageUrl;
// ? originalImageUrl.replace(/^(.*)\.([^\.]*)$/g, '$1_' + format + '.$2') : '';
},
Image: {
imageSize: function imageSize(t) {
var e = t.match(/.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\.@]/);
return null !== e ? e[1] : null;
},
getSizedImageUrl: function getSizedImageUrl(t, e) {
if (null == e)
return t;
if ("master" == e)
return this.removeProtocol(t);
var o = t.match(/\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i);
if (null != o) {
var i = t.split(o[0]),
r = o[0];
return this.removeProtocol(i[0] + "_" + e + r);
}
return null;
},
removeProtocol: function removeProtocol(t) {
return t.replace(/http(s)?:/, "");
} } };
theme.Disclosure = function () {
var selectors = {
disclosureList: '[data-disclosure-list]',
disclosureToggle: '[data-disclosure-toggle]',
disclosureInput: '[data-disclosure-input]',
disclosureOptions: '[data-disclosure-option]' };
var classes = {
listVisible: 'disclosure-list--visible' };
function Disclosure($disclosure) {
this.$container = $disclosure;
this.cache = {};
this._cacheSelectors();
this._connectOptions();
this._connectToggle();
this._onFocusOut();
}
Disclosure.prototype = $.extend({}, Disclosure.prototype, {
_cacheSelectors: function _cacheSelectors() {
this.cache = {
$disclosureList: this.$container.find(selectors.disclosureList),
$disclosureToggle: this.$container.find(selectors.disclosureToggle),
$disclosureInput: this.$container.find(selectors.disclosureInput),
$disclosureOptions: this.$container.find(selectors.disclosureOptions) };
},
_connectToggle: function _connectToggle() {
this.cache.$disclosureToggle.on(
'click',
function (evt) {
var ariaExpanded =
$(evt.currentTarget).attr('aria-expanded') === 'true';
$(evt.currentTarget).attr('aria-expanded', !ariaExpanded);
this.cache.$disclosureList.toggleClass(classes.listVisible);
}.bind(this));
},
_connectOptions: function _connectOptions() {
this.cache.$disclosureOptions.on(
'click',
function (evt) {
evt.preventDefault();
this._submitForm($(evt.currentTarget).data('value'));
}.bind(this));
},
_onFocusOut: function _onFocusOut() {
this.cache.$disclosureToggle.on(
'focusout',
function (evt) {
var disclosureLostFocus =
this.$container.has(evt.relatedTarget).length === 0;
if (disclosureLostFocus) {
this._hideList();
}
}.bind(this));
this.cache.$disclosureList.on(
'focusout',
function (evt) {
var childInFocus =
$(evt.currentTarget).has(evt.relatedTarget).length > 0;
var isVisible = this.cache.$disclosureList.hasClass(
classes.listVisible);
if (isVisible && !childInFocus) {
this._hideList();
}
}.bind(this));
this.$container.on(
'keyup',
function (evt) {
if (evt.which !== 27) return; // escape
this._hideList();
this.cache.$disclosureToggle.focus();
}.bind(this));
this.bodyOnClick = function (evt) {
var isOption = this.$container.has(evt.target).length > 0;
var isVisible = this.cache.$disclosureList.hasClass(
classes.listVisible);
if (isVisible && !isOption) {
this._hideList();
}
}.bind(this);
$('body').on('click', this.bodyOnClick);
},
_submitForm: function _submitForm(value) {
this.cache.$disclosureInput.val(value);
this.$container.parents('form').submit();
},
_hideList: function _hideList() {
this.cache.$disclosureList.removeClass(classes.listVisible);
this.cache.$disclosureToggle.attr('aria-expanded', false);
},
unload: function unload() {
$('body').off('click', this.bodyOnClick);
this.cache.$disclosureOptions.off();
this.cache.$disclosureToggle.off();
this.cache.$disclosureList.off();
this.$container.off();
} });
return Disclosure;
}();
(function () {
function throttle(callback, threshold) {
var debounceTimeoutId = -1;
var tick = false;
return function () {
clearTimeout(debounceTimeoutId);
debounceTimeoutId = setTimeout(callback, threshold);
if (!tick) {
callback.call();
tick = true;
setTimeout(function () {
tick = false;
}, threshold);
}
};
}
var scrollEvent = document.createEvent('Event');
scrollEvent.initEvent('throttled-scroll', true, true);
window.addEventListener("scroll", throttle(function () {
window.dispatchEvent(scrollEvent);
}, 200));
})();
// Source: https://davidwalsh.name/javascript-debounce-function
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
theme.debounce = function (func) {var wait = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 700;var immediate = arguments.length > 2 ? arguments[2] : undefined;
var timeout;
return function () {
var context = this,args = arguments;
var later = function later() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
// requires: throttled-scroll, debouncedresize
/*
Define a section by creating a new function object and registering it with the section handler.
The section handler manages:
Instantiation for all sections on the current page
Theme editor lifecycle events
Deferred initialisation
Event cleanup
There are two ways to register a section.
In a theme:
theme.Sections.register('slideshow', theme.SlideshowSection);
theme.Sections.register('header', theme.HeaderSection, { deferredLoad: false });
theme.Sections.register('background-video', theme.VideoManager, { deferredLoadViewportExcess: 800 });
As a component:
cc.sections.push({ name: 'faq', section: theme.Faq });
Assign any of these to receive Shopify section lifecycle events:
this.onSectionLoad
this.afterSectionLoadCallback
this.onSectionSelect
this.onSectionDeselect
this.onBlockSelect
this.onBlockDeselect
this.onSectionUnload
this.afterSectionUnloadCallback
this.onSectionReorder
If you add any events using the manager's registerEventListener,
e.g. this.registerEventListener(element, 'click', this.functions.handleClick.bind(this)),
these will be automatically cleaned up after onSectionUnload.
*/
theme.Sections = new function () {
var _ = this;
_._instances = [];
_._deferredSectionTargets = [];
_._sections = [];
_._deferredLoadViewportExcess = 300; // load defferred sections within this many px of viewport
_._deferredWatcherRunning = false;
_.init = function () {
$(document).on('shopify:section:load', function (e) {
// load a new section
var target = _._themeSectionTargetFromShopifySectionTarget(e.target);
if (target) {
_.sectionLoad(target);
}
}).on('shopify:section:unload', function (e) {
// unload existing section
var target = _._themeSectionTargetFromShopifySectionTarget(e.target);
if (target) {
_.sectionUnload(target);
}
}).on('shopify:section:reorder', function (e) {
// unload existing section
var target = _._themeSectionTargetFromShopifySectionTarget(e.target);
if (target) {
_.sectionReorder(target);
}
});
$(window).on('throttled-scroll.themeSectionDeferredLoader debouncedresize.themeSectionDeferredLoader', _._processDeferredSections);
_._deferredWatcherRunning = true;
};
// register a type of section
_.register = function (type, section, options) {
_._sections.push({
type: type,
section: section,
afterSectionLoadCallback: options ? options.afterLoad : null,
afterSectionUnloadCallback: options ? options.afterUnload : null });
// load now
$('[data-section-type="' + type + '"]').each(function () {
if (Shopify.designMode || options && options.deferredLoad === false || !_._deferredWatcherRunning) {
_.sectionLoad(this);
} else {
_.sectionDeferredLoad(this, options);
}
});
};
// prepare a section to load later
_.sectionDeferredLoad = function (target, options) {
_._deferredSectionTargets.push({
target: target,
deferredLoadViewportExcess: options && options.deferredLoadViewportExcess ? options.deferredLoadViewportExcess : _._deferredLoadViewportExcess });
_._processDeferredSections(true);
};
// load deferred sections if in/near viewport
_._processDeferredSections = function (firstRunCheck) {
if (_._deferredSectionTargets.length) {
var viewportTop = $(window).scrollTop(),
viewportBottom = viewportTop + $(window).height(),
loopStart = firstRunCheck === true ? _._deferredSectionTargets.length - 1 : 0;
for (var i = loopStart; i < _._deferredSectionTargets.length; i++) {
var target = _._deferredSectionTargets[i].target,
viewportExcess = _._deferredSectionTargets[i].deferredLoadViewportExcess,
sectionTop = $(target).offset().top - viewportExcess,
doLoad = sectionTop > viewportTop && sectionTop < viewportBottom;
if (!doLoad) {
var sectionBottom = sectionTop + $(target).outerHeight() + viewportExcess * 2;
doLoad = sectionBottom > viewportTop && sectionBottom < viewportBottom;
}
if (doLoad || sectionTop < viewportTop && sectionBottom > viewportBottom) {
// in viewport, load
_.sectionLoad(target);
// remove from deferred queue and resume checks
_._deferredSectionTargets.splice(i, 1);
i--;
}
}
}
// remove event if no more deferred targets left, if not on first run
if (firstRunCheck !== true && _._deferredSectionTargets.length === 0) {
_._deferredWatcherRunning = false;
$(window).off('.themeSectionDeferredLoader');
}
};
// load in a section
_.sectionLoad = function (target) {
var target = target,
sectionObj = _._sectionForTarget(target),
section = false;
if (sectionObj.section) {
section = sectionObj.section;
} else {
section = sectionObj;
}
if (section !== false) {
var instance = {
target: target,
section: section,
$shopifySectionContainer: $(target).closest('.shopify-section'),
thisContext: {
functions: section.functions,
registeredEventListeners: [] } };
instance.thisContext.registerEventListener = _._registerEventListener.bind(instance.thisContext);
_._instances.push(instance);
//Initialise any components
if ($(target).data('components')) {
//Init each component
var components = $(target).data('components').split(',');
components.forEach(component => {
$(document).trigger('cc:component:load', [component, target]);
});
}
_._callSectionWith(section, 'onSectionLoad', target, instance.thisContext);
_._callSectionWith(section, 'afterSectionLoadCallback', target, instance.thisContext);
// attach additional UI events if defined
if (section.onSectionSelect) {
instance.$shopifySectionContainer.on('shopify:section:select', function (e) {
_._callSectionWith(section, 'onSectionSelect', e.target, instance.thisContext);
});
}
if (section.onSectionDeselect) {
instance.$shopifySectionContainer.on('shopify:section:deselect', function (e) {
_._callSectionWith(section, 'onSectionDeselect', e.target, instance.thisContext);
});
}
if (section.onBlockSelect) {
$(target).on('shopify:block:select', function (e) {
_._callSectionWith(section, 'onBlockSelect', e.target, instance.thisContext);
});
}
if (section.onBlockDeselect) {
$(target).on('shopify:block:deselect', function (e) {
_._callSectionWith(section, 'onBlockDeselect', e.target, instance.thisContext);
});
}
}
};
// unload a section
_.sectionUnload = function (target) {
var sectionObj = _._sectionForTarget(target);
var instanceIndex = -1;
for (var i = 0; i < _._instances.length; i++) {
if (_._instances[i].target == target) {
instanceIndex = i;
}
}
if (instanceIndex > -1) {
var instance = _._instances[instanceIndex];
// remove events and call unload, if loaded
$(target).off('shopify:block:select shopify:block:deselect');
instance.$shopifySectionContainer.off('shopify:section:select shopify:section:deselect');
_._callSectionWith(instance.section, 'onSectionUnload', target, instance.thisContext);
_._unloadRegisteredEventListeners(instance.thisContext.registeredEventListeners);
_._callSectionWith(sectionObj, 'afterSectionUnloadCallback', target, instance.thisContext);
_._instances.splice(instanceIndex);
//Destroy any components
if ($(target).data('components')) {
//Init each component
var components = $(target).data('components').split(',');
components.forEach(component => {
$(document).trigger('cc:component:unload', [component, target]);
});
}
} else {
// check if it was a deferred section
for (var i = 0; i < _._deferredSectionTargets.length; i++) {
if (_._deferredSectionTargets[i].target == target) {
_._deferredSectionTargets[i].splice(i, 1);
break;
}
}
}
};
_.sectionReorder = function (target) {
var instanceIndex = -1;
for (var i = 0; i < _._instances.length; i++) {
if (_._instances[i].target == target) {
instanceIndex = i;
}
}
if (instanceIndex > -1) {
var instance = _._instances[instanceIndex];
_._callSectionWith(instance.section, 'onSectionReorder', target, instance.thisContext);
}
};
// Helpers
_._registerEventListener = function (element, eventType, callback) {
element.addEventListener(eventType, callback);
this.registeredEventListeners.push({
element,
eventType,
callback });
};
_._unloadRegisteredEventListeners = function (registeredEventListeners) {
registeredEventListeners.forEach(rel => {
rel.element.removeEventListener(rel.eventType, rel.callback);
});
};
_._callSectionWith = function (section, method, container, thisContext) {
if (typeof section[method] === 'function') {
try {
if (thisContext) {
section[method].bind(thisContext)(container);
} else {
section[method](container);
}
} catch (ex) {
var sectionType = container.dataset['sectionType'];
console.warn("Theme warning: '".concat(method, "' failed for section '").concat(sectionType, "'"));
console.debug(container, ex);
}
}
};
_._themeSectionTargetFromShopifySectionTarget = function (target) {
var $target = $('[data-section-type]:first', target);
if ($target.length > 0) {
return $target[0];
} else {
return false;
}
};
_._sectionForTarget = function (target) {
var type = $(target).attr('data-section-type');
for (var i = 0; i < _._sections.length; i++) {
if (_._sections[i].type == type) {
return _._sections[i];
}
}
return false;
};
_._sectionAlreadyRegistered = function (type) {
for (var i = 0; i < _._sections.length; i++) {
if (_._sections[i].type == type) {
return true;
}
}
return false;
};
}();
// Loading third party scripts
theme.scriptsLoaded = {};
theme.loadScriptOnce = function (src, callback, beforeRun, sync) {
if (typeof theme.scriptsLoaded[src] === 'undefined') {
theme.scriptsLoaded[src] = [];
var tag = document.createElement('script');
tag.src = src;
if (sync || beforeRun) {
tag.async = false;
}
if (beforeRun) {
beforeRun();
}
if (typeof callback === 'function') {
theme.scriptsLoaded[src].push(callback);
if (tag.readyState) {// IE, incl. IE9
tag.onreadystatechange = function () {
if (tag.readyState == "loaded" || tag.readyState == "complete") {
tag.onreadystatechange = null;
for (var i = 0; i < theme.scriptsLoaded[this].length; i++) {
theme.scriptsLoaded[this][i]();
}
theme.scriptsLoaded[this] = true;
}
}.bind(src);
} else {
tag.onload = function () {// Other browsers
for (var i = 0; i < theme.scriptsLoaded[this].length; i++) {
theme.scriptsLoaded[this][i]();
}
theme.scriptsLoaded[this] = true;
}.bind(src);
}
}
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
return true;
} else if (typeof theme.scriptsLoaded[src] === 'object' && typeof callback === 'function') {
theme.scriptsLoaded[src].push(callback);
} else {
if (typeof callback === 'function') {
callback();
}
return false;
}
};
theme.loadStyleOnce = function (src) {
var srcWithoutProtocol = src.replace(/^https?:/, '');
if (!document.querySelector('link[href="' + encodeURI(srcWithoutProtocol) + '"]')) {
var tag = document.createElement('link');
tag.href = srcWithoutProtocol;
tag.rel = 'stylesheet';
tag.type = 'text/css';
var firstTag = document.getElementsByTagName('link')[0];
firstTag.parentNode.insertBefore(tag, firstTag);
}
}; /// Show a short-lived text popup above an element
theme.showQuickPopup = function (message, $origin) {
var $popup = $('
');
var offs = $origin.offset();
$popup.html(message).css({ 'left': offs.left, 'top': offs.top }).hide();
$('body').append($popup);
$popup.css({ marginTop: -$popup.outerHeight() - 10, marginLeft: -($popup.outerWidth() - $origin.outerWidth()) / 2 });
$popup.fadeIn(200).delay(3500).fadeOut(400, function () {
$(this).remove();
});
};
class ccComponent {
constructor(name) {var cssSelector = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ".cc-".concat(name);
var _this = this;
this.instances = [];
// Initialise any instance of this component within a section
$(document).on('cc:component:load', function (event, component, target) {
if (component === name) {
$(target).find("".concat(cssSelector, ":not(.cc-initialized)")).each(function () {
_this.init(this);
});
}
});
// Destroy any instance of this component within a section
$(document).on('cc:component:unload', function (event, component, target) {
if (component === name) {
$(target).find(cssSelector).each(function () {
_this.destroy(this);
});
}
});
// Initialise any instance of this component
$(cssSelector).each(function () {
_this.init(this);
});
}
init(container) {
$(container).addClass('cc-initialized');
}
destroy(container) {
$(container).removeClass('cc-initialized');
}
registerInstance(container, instance) {
this.instances.push({
container,
instance });
}
destroyInstance(container) {
this.instances = this.instances.filter(item => {
if (item.container === container) {
if (typeof item.instance.destroy === 'function') {
item.instance.destroy();
}
return item.container !== container;
}
});
}}
/**
* Use with template literals to build HTML with correct escaping.
*
* Example:
*
* const tve = theme.createTemplateVariableEncoder();
* tve.add('className', className, 'attribute');
* tve.add('title', title, 'html');
* tve.add('richText', richText, 'raw');
* const template = `
*
*
${tve.values.title}
*
${tve.values.richText}
*
* `;
*/
theme.createTemplateVariableEncoder = function () {
return {
utilityElement: document.createElement('div'),
values: {},
/**
* Add a new value to sanitise.
* @param {String} key - key used to retrieve this value
* @param {String} value - the value to encode and store
* @param {String} type - possible values: [attribute, html, raw] - the type of encoding to use
*/
add: function add(key, value, type) {
switch (type) {
case 'attribute':
this.utilityElement.innerHTML = '';
this.utilityElement.setAttribute('util', value);
this.values[key] = this.utilityElement.outerHTML.match(/util="([^"]*)"/)[1];
break;
case 'html':
this.utilityElement.innerText = value;
this.values[key] = this.utilityElement.innerHTML;
break;
case 'raw':
this.values[key] = value;
break;
default:
throw "Type '".concat(type, "' not handled");}
} };
};
theme.suffixIds = function (container, suffix) {
var refAttrs = ['aria-describedby', 'aria-controls'];
suffix = '-' + suffix;
container.querySelectorAll('[id]').forEach(el => {
var oldId = el.id,
newId = oldId + suffix;
el.id = newId;
refAttrs.forEach(attr => {
container.querySelectorAll("[".concat(attr, "=\"").concat(oldId, "\"]")).forEach(refEl => {
refEl.setAttribute(attr, newId);
});
});
});
};
theme.renderUnitPrice = function (unit_price, unit_price_measurement, money_format) {
if (unit_price && unit_price_measurement) {
var unitPriceHtml = '
';
unitPriceHtml += "".concat(theme.Shopify.formatMoney(unit_price, money_format), "");
unitPriceHtml += "".concat(theme.strings.products_product_unit_price_separator, "");
var unit = unit_price_measurement.reference_unit;
if (unit_price_measurement.reference_value != 1) {
unit = unit_price_measurement.reference_value + unit;
}
unitPriceHtml += "".concat(unit, "");
unitPriceHtml += '
';
return unitPriceHtml;
} else {
return '';
}
};
class ccPopup {
constructor($container, namespace) {
this.$container = $container;
this.namespace = namespace;
this.cssClasses = {
visible: 'cc-popup--visible',
bodyNoScroll: 'cc-popup-no-scroll',
bodyNoScrollPadRight: 'cc-popup-no-scroll-pad-right' };
}
/**
* Open popup on timer / local storage - move focus to input ensure you can tab to submit and close
* Add the cc-popup--visible class
* Update aria to visible
*/
open(callback) {
// Prevent the body from scrolling
if (this.$container.data('freeze-scroll')) {
$('body').addClass(this.cssClasses.bodyNoScroll);
// Add any padding necessary to the body to compensate for the scrollbar that just disappeared
var scrollDiv = document.createElement('div');
scrollDiv.className = 'popup-scrollbar-measure';
document.body.appendChild(scrollDiv);
var scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;
document.body.removeChild(scrollDiv);
if (scrollbarWidth > 0) {
$('body').css('padding-right', scrollbarWidth + 'px').addClass(this.cssClasses.bodyNoScrollPadRight);
}
}
// Add reveal class
this.$container.addClass(this.cssClasses.visible);
// Track previously focused element
this.previouslyActiveElement = document.activeElement;
// Focus on the close button after the animation in has completed
setTimeout(() => {
this.$container.find('.cc-popup-close')[0].focus();
}, 500);
// Pressing escape closes the modal
$(window).on('keydown' + this.namespace, event => {
if (event.keyCode === 27) {
this.close();
}
});
if (callback) {
callback();
}
}
/**
* Close popup on click of close button or background - where does the focus go back to?
* Remove the cc-popup--visible class
*/
close(callback) {
// Remove reveal class
this.$container.removeClass(this.cssClasses.visible);
// Revert focus
if (this.previouslyActiveElement) {
$(this.previouslyActiveElement).focus();
}
// Destroy the escape event listener
$(window).off('keydown' + this.namespace);
// Allow the body to scroll and remove any scrollbar-compensating padding
if (this.$container.data('freeze-scroll')) {
var transitionDuration = 500;
var $innerModal = this.$container.find('.cc-popup-modal');
if ($innerModal.length) {
transitionDuration = parseFloat(getComputedStyle($innerModal[0])['transitionDuration']);
if (transitionDuration && transitionDuration > 0) {
transitionDuration *= 1000;
}
}
setTimeout(() => {
$('body').removeClass(this.cssClasses.bodyNoScroll).removeClass(this.cssClasses.bodyNoScrollPadRight).css('padding-right', '0');
}, transitionDuration);
}
if (callback) {
callback();
}
}}
;
(() => {
theme.initAnimateOnScroll = function () {
if (document.body.classList.contains('cc-animate-enabled') && window.innerWidth >= 768) {
var animationTimeout = typeof document.body.dataset.ccAnimateTimeout !== "undefined" ? document.body.dataset.ccAnimateTimeout : 200;
if ('IntersectionObserver' in window) {
var intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
// In view and hasn't been animated yet
if (entry.isIntersecting && !entry.target.classList.contains("cc-animate-complete")) {
setTimeout(() => {
entry.target.classList.add("-in", "cc-animate-complete");
}, animationTimeout);
setTimeout(() => {
//Once the animation is complete (assume 5 seconds), remove the animate attribute to remove all css
entry.target.classList.remove("data-cc-animate");
entry.target.style.transitionDuration = null;
entry.target.style.transitionDelay = null;
}, 5000);
// Remove observer after animation
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('[data-cc-animate]:not(.cc-animate-init)').forEach(elem => {
//Set the animation delay
if (elem.dataset.ccAnimateDelay) {
elem.style.transitionDelay = elem.dataset.ccAnimateDelay;
}
///Set the animation duration
if (elem.dataset.ccAnimateDuration) {
elem.style.transitionDuration = elem.dataset.ccAnimateDuration;
}
//Init the animation
if (elem.dataset.ccAnimate) {
elem.classList.add(elem.dataset.ccAnimate);
}
elem.classList.add("cc-animate-init");
//Watch for elem
intersectionObserver.observe(elem);
});
} else {
//Fallback, load all the animations now
var elems = document.querySelectorAll('[data-cc-animate]:not(.cc-animate-init)');
for (var i = 0; i < elems.length; i++) {
elems[i].classList.add("-in", "cc-animate-complete");
}
}
}
};
theme.initAnimateOnScroll();
document.addEventListener('shopify:section:load', () => {
setTimeout(theme.initAnimateOnScroll, 100);
});
//Reload animations when changing from mobile to desktop
try {
window.matchMedia('(min-width: 768px)').addEventListener('change', event => {
if (event.matches) {
setTimeout(theme.initAnimateOnScroll, 100);
}
});
} catch (e) {}
})();
class AccordionInstance {
constructor(container) {
this.accordion = container;
this.itemClass = '.cc-accordion-item';
this.titleClass = '.cc-accordion-item__title';
this.panelClass = '.cc-accordion-item__panel';
this.allowMultiOpen = this.accordion.dataset.allowMultiOpen === 'true';
// If multiple open items not allowed, set open item as active (if there is one)
if (!this.allowMultiOpen) {
this.activeItem = this.accordion.querySelector("".concat(this.itemClass, "[open]"));
}
this.bindEvents();
}
/**
* Adds inline 'height' style to a panel, to trigger open transition
* @param {HTMLDivElement} panel - The accordion item content panel
*/
static addPanelHeight(panel) {
panel.style.height = "".concat(panel.scrollHeight, "px");
}
/**
* Removes inline 'height' style from a panel, to trigger close transition
* @param {HTMLDivElement} panel - The accordion item content panel
*/
static removePanelHeight(panel) {
panel.getAttribute('style'); // Fix Safari bug (doesn't remove attribute without this first!)
panel.removeAttribute('style');
}
/**
* Opens an accordion item
* @param {HTMLDetailsElement} item - The accordion item
* @param {HTMLDivElement} panel - The accordion item content panel
*/
open(item, panel) {
panel.style.height = '0';
// Set item to open. Blocking the default click action and opening it this way prevents a
// slight delay which causes the panel height to be set to '0' (because item's not open yet)
item.open = true;
AccordionInstance.addPanelHeight(panel);
// Slight delay required before starting transitions
setTimeout(() => {
item.classList.add('is-open');
}, 10);
if (!this.allowMultiOpen) {
// If there's an active item and it's not the opened item, close it
if (this.activeItem && this.activeItem !== item) {
var activePanel = this.activeItem.querySelector(this.panelClass);
this.close(this.activeItem, activePanel);
}
this.activeItem = item;
}
}
/**
* Closes an accordion item
* @param {HTMLDetailsElement} item - The accordion item
* @param {HTMLDivElement} panel - The accordion item content panel
*/
close(item, panel) {
AccordionInstance.addPanelHeight(panel);
item.classList.remove('is-open');
item.classList.add('is-closing');
if (this.activeItem === item) {
this.activeItem = null;
}
// Slight delay required to allow scroll height to be applied before changing to '0'
setTimeout(() => {
panel.style.height = '0';
}, 10);
}
/**
* Handles 'click' event on the accordion
* @param {Object} e - The event object
*/
handleClick(e) {
// Ignore clicks outside a toggle ( element)
var toggle = e.target.closest(this.titleClass);
if (!toggle) return;
// Prevent the default action
// We'll trigger it manually after open transition initiated or close transition complete
e.preventDefault();
var item = toggle.parentNode;
var panel = toggle.nextElementSibling;
if (item.open) {
this.close(item, panel);
} else {
this.open(item, panel);
}
}
/**
* Handles 'transitionend' event in the accordion
* @param {Object} e - The event object
*/
handleTransition(e) {
// Ignore transitions not on a panel element
if (!e.target.matches(this.panelClass)) return;
var panel = e.target;
var item = panel.parentNode;
if (item.classList.contains('is-closing')) {
item.classList.remove('is-closing');
item.open = false;
}
AccordionInstance.removePanelHeight(panel);
}
bindEvents() {
// Need to assign the function calls to variables because bind creates a new function,
// which means the event listeners can't be removed in the usual way
this.clickHandler = this.handleClick.bind(this);
this.transitionHandler = this.handleTransition.bind(this);
this.accordion.addEventListener('click', this.clickHandler);
this.accordion.addEventListener('transitionend', this.transitionHandler);
}
destroy() {
this.accordion.removeEventListener('click', this.clickHandler);
this.accordion.removeEventListener('transitionend', this.transitionHandler);
}}
class Accordion extends ccComponent {
constructor() {var name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'accordion';var cssSelector = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ".cc-".concat(name);
super(name, cssSelector);
}
init(container) {
super.init(container);
this.registerInstance(container, new AccordionInstance(container));
}
destroy(container) {
this.destroyInstance(container);
super.destroy(container);
}}
new Accordion();
class CustomSelectInstance {
constructor(el) {
this.el = el;
this.button = el.querySelector('.cc-select__btn');
this.listbox = el.querySelector('.cc-select__listbox');
this.options = el.querySelectorAll('.cc-select__option');
this.selectedOption = el.querySelector('[aria-selected="true"]');
this.nativeSelect = document.getElementById("".concat(el.id, "-native"));
this.swatches = 'swatch' in this.options[this.options.length - 1].dataset;
this.focusedClass = 'is-focused';
this.searchString = '';
this.listboxOpen = false;
// Set the selected option
if (!this.selectedOption) {
this.selectedOption = this.listbox.firstElementChild;
}
this.bindEvents();
this.setButtonWidth();
}
bindEvents() {
this.el.addEventListener('keydown', this.handleKeydown.bind(this));
this.button.addEventListener('mousedown', this.handleMousedown.bind(this));
}
/**
* Adds event listeners when the options list is visible
*/
addListboxOpenEvents() {
this.mouseoverHandler = this.handleMouseover.bind(this);
this.mouseleaveHandler = this.handleMouseleave.bind(this);
this.clickHandler = this.handleClick.bind(this);
this.blurHandler = this.handleBlur.bind(this);
this.listbox.addEventListener('mouseover', this.mouseoverHandler);
this.listbox.addEventListener('mouseleave', this.mouseleaveHandler);
this.listbox.addEventListener('click', this.clickHandler);
this.listbox.addEventListener('blur', this.blurHandler);
}
/**
* Removes event listeners added when the options list was visible
*/
removeListboxOpenEvents() {
this.listbox.removeEventListener('mouseover', this.mouseoverHandler);
this.listbox.removeEventListener('mouseleave', this.mouseleaveHandler);
this.listbox.removeEventListener('click', this.clickHandler);
this.listbox.removeEventListener('blur', this.blurHandler);
}
/**
* Handles a 'keydown' event on the custom select element
* @param {Object} e - The event object
*/
handleKeydown(e) {
if (this.listboxOpen) {
this.handleKeyboardNav(e);
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === ' ') {
e.preventDefault();
this.showListbox();
}
}
/**
* Handles a 'mousedown' event on the button element
* @param {Object} e - The event object
*/
handleMousedown(e) {
if (!this.listboxOpen && e.button === 0) {
this.showListbox();
}
}
/**
* Handles a 'mouseover' event on the options list
* @param {Object} e - The event object
*/
handleMouseover(e) {
if (e.target.matches('li')) {
this.focusOption(e.target);
}
}
/**
* Handles a 'mouseleave' event on the options list
*/
handleMouseleave() {
this.focusOption(this.selectedOption);
}
/**
* Handles a 'click' event on the options list
* @param {Object} e - The event object
*/
handleClick(e) {
if (e.target.matches('.js-option')) {
this.selectOption(e.target);
}
}
/**
* Handles a 'blur' event on the options list
*/
handleBlur() {
if (this.listboxOpen) {
this.hideListbox();
}
}
/**
* Handles a 'keydown' event on the options list
* @param {Object} e - The event object
*/
handleKeyboardNav(e) {
var optionToFocus;
// Disable tabbing if options list is open (as per native select element)
if (e.key === 'Tab') {
e.preventDefault();
}
switch (e.key) {
// Focus an option
case 'ArrowUp':
case 'ArrowDown':
e.preventDefault();
if (e.key === 'ArrowUp') {
optionToFocus = this.focusedOption.previousElementSibling;
} else {
optionToFocus = this.focusedOption.nextElementSibling;
}
if (optionToFocus && !optionToFocus.classList.contains('is-disabled')) {
this.focusOption(optionToFocus);
}
break;
// Select an option
case 'Enter':
case ' ':
e.preventDefault();
this.selectOption(this.focusedOption);
break;
// Cancel and close the options list
case 'Escape':
e.preventDefault();
this.hideListbox();
break;
// Search for an option and focus the first match (if one exists)
default:
optionToFocus = this.findOption(e.key);
if (optionToFocus) {
this.focusOption(optionToFocus);
}
break;}
}
/**
* Sets the button width to the same as the longest option, to prevent
* the button width from changing depending on the option selected
*/
setButtonWidth() {
// Get the width of an element without side padding
var getUnpaddedWidth = el => {
var elStyle = getComputedStyle(el);
return parseFloat(elStyle.paddingLeft) + parseFloat(elStyle.paddingRight);
};
var buttonPadding = getUnpaddedWidth(this.button);
var optionPadding = getUnpaddedWidth(this.selectedOption);
var buttonBorder = this.button.offsetWidth - this.button.clientWidth;
var optionWidth = Math.ceil(this.selectedOption.getBoundingClientRect().width);
this.button.style.width = "".concat(optionWidth - optionPadding + buttonPadding + buttonBorder, "px");
}
/**
* Shows the options list
*/
showListbox() {
this.listbox.hidden = false;
this.listboxOpen = true;
this.el.classList.add('is-open');
this.button.setAttribute('aria-expanded', 'true');
this.listbox.setAttribute('aria-hidden', 'false');
// Slight delay required to prevent blur event being fired immediately
setTimeout(() => {
this.focusOption(this.selectedOption);
this.listbox.focus();
this.addListboxOpenEvents();
}, 10);
}
/**
* Hides the options list
*/
hideListbox() {
if (!this.listboxOpen) return;
this.listbox.hidden = true;
this.listboxOpen = false;
this.el.classList.remove('is-open');
this.button.setAttribute('aria-expanded', 'false');
this.listbox.setAttribute('aria-hidden', 'true');
if (this.focusedOption) {
this.focusedOption.classList.remove(this.focusedClass);
this.focusedOption = null;
}
this.button.focus();
this.removeListboxOpenEvents();
}
/**
* Finds a matching option from a typed string
* @param {string} key - The key pressed
* @returns {?HTMLElement}
*/
findOption(key) {
this.searchString += key;
// If there's a timer already running, clear it
if (this.searchTimer) {
clearTimeout(this.searchTimer);
}
// Wait 500ms to see if another key is pressed, if not then clear the search string
this.searchTimer = setTimeout(() => {
this.searchString = '';
}, 500);
// Find an option that contains the search string (if there is one)
var matchingOption = [...this.options].find(option => {
var label = option.innerText.toLowerCase();
return label.includes(this.searchString) && !option.classList.contains('is-disabled');
});
return matchingOption;
}
/**
* Focuses an option
* @param {HTMLElement} option - The
element of the option to focus
*/
focusOption(option) {
// Remove focus on currently focused option (if there is one)
if (this.focusedOption) {
this.focusedOption.classList.remove(this.focusedClass);
}
// Set focus on the option
this.focusedOption = option;
this.focusedOption.classList.add(this.focusedClass);
// If option is out of view, scroll the list
if (this.listbox.scrollHeight > this.listbox.clientHeight) {
var scrollBottom = this.listbox.clientHeight + this.listbox.scrollTop;
var optionBottom = option.offsetTop + option.offsetHeight;
if (optionBottom > scrollBottom) {
this.listbox.scrollTop = optionBottom - this.listbox.clientHeight;
} else if (option.offsetTop < this.listbox.scrollTop) {
this.listbox.scrollTop = option.offsetTop;
}
}
}
/**
* Selects an option
* @param {HTMLElement} option - The option
element
*/
selectOption(option) {
if (option !== this.selectedOption) {
// Switch aria-selected attribute to selected option
option.setAttribute('aria-selected', 'true');
this.selectedOption.setAttribute('aria-selected', 'false');
// Update swatch colour in the button
if (this.swatches) {
if (option.dataset.swatch) {
this.button.dataset.swatch = option.dataset.swatch;
} else {
this.button.removeAttribute('data-swatch');
}
}
// Update the button text and set the option as active
this.button.firstChild.textContent = option.firstElementChild.textContent;
this.listbox.setAttribute('aria-activedescendant', option.id);
this.selectedOption = document.getElementById(option.id);
// If a native select element exists, update its selected value and trigger a 'change' event
if (this.nativeSelect) {
this.nativeSelect.value = option.dataset.value;
this.nativeSelect.dispatchEvent(new Event('change', { bubbles: true }));
} else {
// Trigger a 'change' event on the custom select element
var detail = { selectedValue: option.dataset.value };
this.el.dispatchEvent(new CustomEvent('change', { bubbles: true, detail }));
}
}
this.hideListbox();
}}
class CustomSelect extends ccComponent {
constructor() {var name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'custom-select';var cssSelector = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ".cc-select";
super(name, cssSelector);
}
init(container) {
super.init(container);
this.registerInstance(container, new CustomSelectInstance(container));
}
destroy(container) {
this.destroyInstance(container);
super.destroy(container);
}}
new CustomSelect();
/**
* Display a modal window on-click. It's a lightbox.
* To use:
* Add 'cc-modal' class to a clickable element.
*
* Configure with:
* - data-cc-modal-contentelement - selector for element containing content to show, innerHTML of element is copied into the modal
* - data-cc-modal-size (optional) - 'small' or 'medium', defaults to 'medium'
* - data-cc-modal-launch (optional) - 'true' if we want to open the modal immediately
*
* Example:
*
*