(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: * *
    */ class ModalInstance { constructor(container) { this.container = container; this.size = container.dataset.ccModalSize || 'medium'; this.contentElement = document.querySelector(container.dataset.ccModalContentelement); this.container.addEventListener('click', this.handleClick.bind(this)); if (container.dataset.ccModalLaunch === 'true') { setTimeout(this.open.bind(this), 10); } } /** * Handles 'click' event on the container * @param {Object} e - The event object */ handleClick(e) { e.preventDefault(); this.open(); this.opener = e.target; } /** * Create the modal and add it to the page. */ open() { var tve = theme.createTemplateVariableEncoder(); tve.add('size', this.size, 'attribute'); tve.add('content', this.contentElement.innerHTML, 'raw'); tve.add('button_close_label', theme.strings.general_accessibility_labels_close, 'attribute'); tve.add('button_close_icon', theme.icons.close, 'raw'); var html = "\n
    \n
    \n
    \n
    \n \n
    ").concat( tve.values.content, "
    \n
    \n
    \n
    \n "); var modalElementFragment = document.createRange().createContextualFragment(html); document.body.appendChild(modalElementFragment); document.body.classList.add('cc-modal-visible'); this.modalElement = document.body.lastElementChild; this.modalElement.querySelector('.cc-modal-window__background').addEventListener('click', this.close.bind(this)); this.modalElement.querySelector('.cc-modal-window__close').addEventListener('click', this.close.bind(this)); setTimeout(() => { this.modalElement.classList.remove('cc-modal-window--pre-reveal'); this.modalElement.querySelector('.cc-modal-window__close').focus(); }, 10); } close() { this.modalElement.classList.add('cc-modal-window--closing'); if (!document.querySelector('.cc-modal-window:not(.cc-modal-window--closing)')) { document.body.classList.remove('cc-modal-visible'); } if (this.opener) { setTimeout(() => { this.opener.focus(); }, 10); } // give transitions 5s to do their thing before we tidy up (note: this.modalElement may be reassigned during this timeout) setTimeout(function () { this.remove(); }.bind(this.modalElement), 5000); }} class Modal extends ccComponent { constructor() {var name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'modal';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 ModalInstance(container)); } destroy(container) { this.destroyInstance(container); super.destroy(container); }} new Modal(); class PriceRangeInstance { constructor(container) { this.container = container; this.selectors = { inputMin: '.cc-price-range__input--min', inputMax: '.cc-price-range__input--max', control: '.cc-price-range__control', controlMin: '.cc-price-range__control--min', controlMax: '.cc-price-range__control--max', bar: '.cc-price-range__bar', activeBar: '.cc-price-range__bar-active' }; this.controls = { min: { barControl: container.querySelector(this.selectors.controlMin), input: container.querySelector(this.selectors.inputMin) }, max: { barControl: container.querySelector(this.selectors.controlMax), input: container.querySelector(this.selectors.inputMax) } }; this.controls.min.value = parseInt(this.controls.min.input.value === '' ? this.controls.min.input.placeholder : this.controls.min.input.value); this.controls.max.value = parseInt(this.controls.max.input.value === '' ? this.controls.max.input.placeholder : this.controls.max.input.value); this.valueMin = this.controls.min.input.min; this.valueMax = this.controls.min.input.max; this.valueRange = this.valueMax - this.valueMin; [this.controls.min, this.controls.max].forEach(item => { item.barControl.setAttribute('aria-valuemin', this.valueMin); item.barControl.setAttribute('aria-valuemax', this.valueMax); item.barControl.setAttribute('tabindex', 0); }); this.controls.min.barControl.setAttribute('aria-valuenow', this.controls.min.value); this.controls.max.barControl.setAttribute('aria-valuenow', this.controls.max.value); this.bar = container.querySelector(this.selectors.bar); this.activeBar = container.querySelector(this.selectors.activeBar); this.inDrag = false; this.bindEvents(); this.render(); } getPxToValueRatio() { return this.bar.clientWidth / (this.valueMax - this.valueMin); } getPcToValueRatio() { return 100.0 / (this.valueMax - this.valueMin); } setActiveControlValue(value) { // only accept valid numbers if (isNaN(parseInt(value))) return; // clamp & default if (this.activeControl === this.controls.min) { if (value === '') { value = this.valueMin; } value = Math.max(this.valueMin, value); value = Math.min(value, this.controls.max.value); } else { if (value === '') { value = this.valueMax; } value = Math.min(this.valueMax, value); value = Math.max(value, this.controls.min.value); } // round this.activeControl.value = Math.round(value); // update input if (this.activeControl.input.value != this.activeControl.value) { if (this.activeControl.value == this.activeControl.input.placeholder) { this.activeControl.input.value = ''; } else { this.activeControl.input.value = this.activeControl.value; } this.activeControl.input.dispatchEvent(new CustomEvent('change', { bubbles: true, cancelable: false, detail: { sender: 'theme:component:price_range' } })); } // a11y this.activeControl.barControl.setAttribute('aria-valuenow', this.activeControl.value); } render() { this.drawControl(this.controls.min); this.drawControl(this.controls.max); this.drawActiveBar(); } drawControl(control) { control.barControl.style.left = (control.value - this.valueMin) * this.getPcToValueRatio() + '%'; } drawActiveBar() { this.activeBar.style.left = (this.controls.min.value - this.valueMin) * this.getPcToValueRatio() + '%'; this.activeBar.style.right = (this.valueMax - this.controls.max.value) * this.getPcToValueRatio() + '%'; } handleControlTouchStart(e) { e.preventDefault(); this.startDrag(e.target, e.touches[0].clientX); this.boundControlTouchMoveEvent = this.handleControlTouchMove.bind(this); this.boundControlTouchEndEvent = this.handleControlTouchEnd.bind(this); window.addEventListener('touchmove', this.boundControlTouchMoveEvent); window.addEventListener('touchend', this.boundControlTouchEndEvent); } handleControlTouchMove(e) { this.moveDrag(e.touches[0].clientX); } handleControlTouchEnd(e) { e.preventDefault(); window.removeEventListener('touchmove', this.boundControlTouchMoveEvent); window.removeEventListener('touchend', this.boundControlTouchEndEvent); this.stopDrag(); } handleControlMouseDown(e) { e.preventDefault(); this.startDrag(e.target, e.clientX); this.boundControlMouseMoveEvent = this.handleControlMouseMove.bind(this); this.boundControlMouseUpEvent = this.handleControlMouseUp.bind(this); window.addEventListener('mousemove', this.boundControlMouseMoveEvent); window.addEventListener('mouseup', this.boundControlMouseUpEvent); } handleControlMouseMove(e) { this.moveDrag(e.clientX); } handleControlMouseUp(e) { e.preventDefault(); window.removeEventListener('mousemove', this.boundControlMouseMoveEvent); window.removeEventListener('mouseup', this.boundControlMouseUpEvent); this.stopDrag(); } startDrag(target, startX) { if (this.controls.min.barControl === target) { this.activeControl = this.controls.min; } else { this.activeControl = this.controls.max; } this.dragStartX = startX; this.dragStartValue = this.activeControl.value; this.inDrag = true; } moveDrag(moveX) { if (this.inDrag) { var value = this.dragStartValue + (moveX - this.dragStartX) / this.getPxToValueRatio(); this.setActiveControlValue(value); this.render(); } } stopDrag() { this.inDrag = false; } handleControlKeyDown(e) { if (e.key === 'ArrowRight') { this.incrementControlFromKeypress(e.target, 10.0); } else if (e.key === 'ArrowLeft') { this.incrementControlFromKeypress(e.target, -10.0); } } incrementControlFromKeypress(control, pxAmount) { if (this.controls.min.barControl === control) { this.activeControl = this.controls.min; } else { this.activeControl = this.controls.max; } this.setActiveControlValue(this.activeControl.value + pxAmount / this.getPxToValueRatio()); this.render(); } handleInputChange(e) { // strip out non numeric values e.target.value = e.target.value.replace(/\D/g, ''); if (!e.detail || e.detail.sender != 'theme:component:price_range') { if (this.controls.min.input === e.target) { this.activeControl = this.controls.min; } else { this.activeControl = this.controls.max; } this.setActiveControlValue(e.target.value); this.render(); } } handleInputKeyup(e) { // enforce numeric chars in the input setTimeout(function () { this.value = this.value.replace(/\D/g, ''); }.bind(e.target), 10); } bindEvents() { [this.controls.min, this.controls.max].forEach(item => { item.barControl.addEventListener('touchstart', this.handleControlTouchStart.bind(this)); item.barControl.addEventListener('mousedown', this.handleControlMouseDown.bind(this)); item.barControl.addEventListener('keydown', this.handleControlKeyDown.bind(this)); item.input.addEventListener('change', this.handleInputChange.bind(this)); item.input.addEventListener('keyup', this.handleInputKeyup.bind(this)); }); } destroy() { }} class PriceRange extends ccComponent { constructor() {var name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'price-range';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 PriceRangeInstance(container)); } destroy(container) { this.destroyInstance(container); super.destroy(container); }} new PriceRange(); // Manage videos theme.VideoManager = new function () { var _ = this; _._permitPlayback = function (container) { return !($(container).hasClass('video-container--background') && $(window).outerWidth() < 768); }; // Youtube _.youtubeVars = { incrementor: 0, apiReady: false, videoData: {}, toProcessSelector: '.video-container[data-video-type="youtube"]:not(.video--init)' }; _.youtubeApiReady = function () { _.youtubeVars.apiReady = true; _._loadYoutubeVideos(); }; _._loadYoutubeVideos = function (container) { if ($(_.youtubeVars.toProcessSelector, container).length) { if (_.youtubeVars.apiReady) { // play those videos $(_.youtubeVars.toProcessSelector, container).each(function () { // Don't init background videos on mobile if (_._permitPlayback($(this))) { $(this).addClass('video--init'); _.youtubeVars.incrementor++; var containerId = 'theme-yt-video-' + _.youtubeVars.incrementor; $(this).data('video-container-id', containerId); var videoElement = $('
    ').attr('id', containerId). appendTo($('.video-container__video', this)); var autoplay = $(this).data('video-autoplay'); var loop = $(this).data('video-loop'); var player = new YT.Player(containerId, { height: '360', width: '640', videoId: $(this).data('video-id'), playerVars: { iv_load_policy: 3, modestbranding: 1, autoplay: 0, loop: loop ? 1 : 0, playlist: $(this).data('video-id'), rel: 0, showinfo: 0 }, events: { onReady: _._onYoutubePlayerReady.bind({ autoplay: autoplay, loop: loop, $container: $(this) }), onStateChange: _._onYoutubePlayerStateChange.bind({ autoplay: autoplay, loop: loop, $container: $(this) }) } }); _.youtubeVars.videoData[containerId] = { id: containerId, container: this, videoElement: videoElement, player: player }; } }); } else { // load api theme.loadScriptOnce('https://www.youtube.com/iframe_api'); } } }; _._onYoutubePlayerReady = function (event) { event.target.setPlaybackQuality('hd1080'); if (this.autoplay) { event.target.mute(); event.target.playVideo(); } _._initBackgroundVideo(this.$container); }; _._onYoutubePlayerStateChange = function (event) { if (event.data == YT.PlayerState.PLAYING) { this.$container.addClass('video--play-started'); if (this.autoplay) { event.target.mute(); } if (this.loop) { // 4 times a second, check if we're in the final second of the video. If so, loop it for a more seamless loop var finalSecond = event.target.getDuration() - 1; if (finalSecond > 2) { function loopTheVideo() { if (event.target.getCurrentTime() > finalSecond) { event.target.seekTo(0); } setTimeout(loopTheVideo, 250); } loopTheVideo(); } } } }; _._unloadYoutubeVideos = function (container) { for (var dataKey in _.youtubeVars.videoData) { var data = _.youtubeVars.videoData[dataKey]; if ($(container).find(data.container).length) { data.player.destroy(); delete _.youtubeVars.videoData[dataKey]; return; } } }; // Vimeo _.vimeoVars = { incrementor: 0, apiReady: false, videoData: {}, toProcessSelector: '.video-container[data-video-type="vimeo"]:not(.video--init)' }; _.vimeoApiReady = function () { _.vimeoVars.apiReady = true; _._loadVimeoVideos(); }; _._loadVimeoVideos = function (container) { if ($(_.vimeoVars.toProcessSelector, container).length) { if (_.vimeoVars.apiReady) { // play those videos $(_.vimeoVars.toProcessSelector, container).each(function () { // Don't init background videos on mobile if (_._permitPlayback($(this))) { $(this).addClass('video--init'); _.vimeoVars.incrementor++; var $this = $(this); var containerId = 'theme-vi-video-' + _.vimeoVars.incrementor; $(this).data('video-container-id', containerId); var videoElement = $('
    ').attr('id', containerId). appendTo($('.video-container__video', this)); var autoplay = !!$(this).data('video-autoplay'); var player = new Vimeo.Player(containerId, { url: $(this).data('video-url'), width: 640, loop: $(this).data('video-autoplay'), autoplay: autoplay, muted: $this.hasClass('video-container--background') || autoplay }); player.on('playing', function () { $(this).addClass('video--play-started'); }.bind(this)); player.ready().then(function () { if (autoplay) { player.setVolume(0); player.play(); } if (player.element && player.element.width && player.element.height) { var ratio = parseInt(player.element.height) / parseInt(player.element.width); $this.find('.video-container__video').css('padding-bottom', ratio * 100 + '%'); } _._initBackgroundVideo($this); }); _.vimeoVars.videoData[containerId] = { id: containerId, container: this, videoElement: videoElement, player: player, autoPlay: autoplay }; } }); } else { // load api if (window.define) { // workaround for third parties using RequireJS theme.loadScriptOnce('https://player.vimeo.com/api/player.js', function () { _.vimeoVars.apiReady = true; _._loadVimeoVideos(); window.define = window.tempDefine; }, function () { window.tempDefine = window.define; window.define = null; }); } else { theme.loadScriptOnce('https://player.vimeo.com/api/player.js', function () { _.vimeoVars.apiReady = true; _._loadVimeoVideos(); }); } } } }; _._unloadVimeoVideos = function (container) { for (var dataKey in _.vimeoVars.videoData) { var data = _.vimeoVars.videoData[dataKey]; if ($(container).find(data.container).length) { data.player.unload(); delete _.vimeoVars.videoData[dataKey]; return; } } }; // Init third party apis - Youtube and Vimeo _._loadThirdPartyApis = function (container) { //Don't init youtube or vimeo background videos on mobile if (_._permitPlayback($('.video-container', container))) { _._loadYoutubeVideos(container); _._loadVimeoVideos(container); } }; // Mp4 _.mp4Vars = { incrementor: 0, videoData: {}, toProcessSelector: '.video-container[data-video-type="mp4"]:not(.video--init)' }; _._loadMp4Videos = function (container) { if ($(_.mp4Vars.toProcessSelector, container).length) { // play those videos $(_.mp4Vars.toProcessSelector, container).addClass('video--init').each(function () { _.mp4Vars.incrementor++; var $this = $(this); var containerId = 'theme-mp-video-' + _.mp4Vars.incrementor; $(this).data('video-container-id', containerId); var videoElement = $('
    ').attr('id', containerId). appendTo($('.video-container__video', this)); var $video = $('"); var htmlFragment = document.createRange().createContextualFragment(html); indexContainer.appendChild(htmlFragment); } } } if (indexContainer) { this.functions.resizeIndex.call(this); } }, resizeIndex: function resizeIndex() { var stickyContainer = this.container.querySelector('.faq-index__sticky-container'), faqHeaderSection = this.container.closest('.section-faq-header'); var currentElement = faqHeaderSection; while (currentElement.nextElementSibling && currentElement.nextElementSibling.classList.contains('section-collapsible-tabs')) { currentElement = currentElement.nextElementSibling; } var stickyContainerRect = stickyContainer.getBoundingClientRect(), currentElementRect = currentElement.getBoundingClientRect(); stickyContainer.style.height = currentElementRect.bottom - stickyContainerRect.top + 'px'; }, performSearch: function performSearch() { // defer to avoid input lag setTimeout(() => { var splitValue = this.searchInput.value.split(' '); // sanitise terms var terms = []; splitValue.forEach(t => { if (t.length > 0) { terms.push(t.toLowerCase()); } }); // search this.linkedQuestionContainers.forEach(element => { if (terms.length) { var termFound = false; var matchContent = element.textContent.toLowerCase(); terms.forEach(term => { if (matchContent.indexOf(term) >= 0) { termFound = true; } }); if (termFound) { element.classList.remove(this.classNames.questionContainerInactive); } else { element.classList.add(this.classNames.questionContainerInactive); } } else { element.classList.remove(this.classNames.questionContainerInactive); } }); // hide non-question content if doing a search this.linkedContent.forEach(element => { if (terms.length) { element.classList.add(this.classNames.questionContainerInactive); } else { element.classList.remove(this.classNames.questionContainerInactive); } }); }, 10); }, handleIndexClick: function handleIndexClick(evt) { if (evt.target.classList.contains('faq-index-item__link')) { evt.preventDefault(); var id = evt.target.href.split('#')[1]; var scrollTarget = document.getElementById(id); var scrollTargetY = scrollTarget.getBoundingClientRect().top + window.pageYOffset - 50; // sticky header offset var stickyHeight = getComputedStyle(document.documentElement).getPropertyValue('--theme-sticky-header-height'); if (stickyHeight) { scrollTargetY -= parseInt(stickyHeight); } window.scrollTo({ top: scrollTargetY, behavior: 'smooth' }); } } }; }(); // Register section cc.sections.push({ name: 'faq-header', section: theme.FaqHeader, deferredLoad: false }); theme.CollapsibleTabs = new function () { this.onSectionLoad = function (container) { this.functions.notifyFaqHeaderOfChange(); }; this.onSectionReorder = function (container) { this.functions.notifyFaqHeaderOfChange(); }; this.onSectionUnload = function (container) { this.functions.notifyFaqHeaderOfChange(); }; this.functions = { notifyFaqHeaderOfChange: function notifyFaqHeaderOfChange() { document.dispatchEvent( new CustomEvent('theme:faq-header-update', { bubbles: true, cancelable: false })); } }; }(); // Register section cc.sections.push({ name: 'collapsible-tabs', section: theme.CollapsibleTabs, deferredLoad: false }); /** * StoreAvailability Section Script * ------------------------------------------------------------------------------ * * @namespace StoreAvailability */ theme.StoreAvailability = function (container) { var loadingClass = 'store-availability-loading'; var initClass = 'store-availability-initialized'; var storageKey = 'cc-location'; this.onSectionLoad = function (container) { this.namespace = theme.namespaceFromSection(container); this.$container = $(container); this.productId = this.$container.data('store-availability-container'); this.sectionUrl = this.$container.data('section-url'); this.$modal; this.$container.addClass(initClass); this.transitionDurationMS = parseFloat(getComputedStyle(container).transitionDuration) * 1000; this.removeFixedHeightTimeout = -1; // Handle when a variant is selected $(window).on("cc-variant-updated".concat(this.namespace).concat(this.productId), (e, args) => { if (args.product.id === this.productId) { this.functions.updateContent.bind(this)( args.variant ? args.variant.id : null, args.product.title, this.$container.data('has-only-default-variant'), args.variant && typeof args.variant.available !== "undefined"); } }); // Handle single variant products if (this.$container.data('single-variant-id')) { this.functions.updateContent.bind(this)( this.$container.data('single-variant-id'), this.$container.data('single-variant-product-title'), this.$container.data('has-only-default-variant'), this.$container.data('single-variant-product-available')); } }; this.onSectionUnload = function () { $(window).off("cc-variant-updated".concat(this.namespace).concat(this.productId)); this.$container.off('click'); if (this.$modal) { this.$modal.off('click'); } }; this.functions = { // Returns the users location data (if allowed) getUserLocation: function getUserLocation() { return new Promise((resolve, reject) => { var storedCoords; if (sessionStorage[storageKey]) { storedCoords = JSON.parse(sessionStorage[storageKey]); } if (storedCoords) { resolve(storedCoords); } else { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( function (position) { var coords = { latitude: position.coords.latitude, longitude: position.coords.longitude }; //Set the localization api fetch('/localization.json', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(coords) }); //Write to a session storage sessionStorage[storageKey] = JSON.stringify(coords); resolve(coords); }, function () { resolve(false); }, { maximumAge: 3600000, // 1 hour timeout: 5000 }); } else { resolve(false); } } }); }, // Requests the available stores and calls the callback getAvailableStores: function getAvailableStores(variantId, cb) { return $.get(this.sectionUrl.replace('VARIANT_ID', variantId), cb); }, // Haversine Distance // The haversine formula is an equation giving great-circle distances between // two points on a sphere from their longitudes and latitudes calculateDistance: function calculateDistance(coords1, coords2, unitSystem) { var dtor = Math.PI / 180; var radius = unitSystem === 'metric' ? 6378.14 : 3959; var rlat1 = coords1.latitude * dtor; var rlong1 = coords1.longitude * dtor; var rlat2 = coords2.latitude * dtor; var rlong2 = coords2.longitude * dtor; var dlon = rlong1 - rlong2; var dlat = rlat1 - rlat2; var a = Math.pow(Math.sin(dlat / 2), 2) + Math.cos(rlat1) * Math.cos(rlat2) * Math.pow(Math.sin(dlon / 2), 2); var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return radius * c; }, // Updates the existing modal pickup with locations with distances from the user updateLocationDistances: function updateLocationDistances(coords) { var unitSystem = this.$modal.find('[data-unit-system]').data('unit-system'); var self = this; this.$modal.find('[data-distance="false"]').each(function () { var thisCoords = { latitude: parseFloat($(this).data('latitude')), longitude: parseFloat($(this).data('longitude')) }; if (thisCoords.latitude && thisCoords.longitude) { var distance = self.functions.calculateDistance( coords, thisCoords, unitSystem).toFixed(1); $(this).html(distance); //Timeout to trigger animation setTimeout(() => { $(this).closest('.store-availability-list__location__distance').addClass('-in'); }, 0); } $(this).attr('data-distance', 'true'); }); }, // Requests the available stores and updates the page with info below Add to Basket, and append the modal to the page updateContent: function updateContent(variantId, productTitle, isSingleDefaultVariant, isVariantAvailable) { this.$container.off('click', '[data-store-availability-modal-open]'); this.$container.off('click' + this.namespace, '.cc-popup-close, .cc-popup-background'); $('.store-availabilities-modal').remove(); if (!isVariantAvailable) { //If the variant is Unavailable (not the same as Out of Stock) - hide the store pickup completely this.$container.addClass(loadingClass); if (this.transitionDurationMS > 0) { this.$container.css('height', '0px'); } } else { this.$container.addClass(loadingClass); if (this.transitionDurationMS > 0) { this.$container.css('height', this.$container.outerHeight() + 'px'); } } if (false) {//isVariantAvailable this.functions.getAvailableStores.call(this, variantId, response => { if (response.trim().length > 0 && !response.includes('NO_PICKUP')) { this.$container.html(response); this.$container.html(this.$container.children().first().html()); // editor bug workaround this.$container.find('[data-store-availability-modal-product-title]').html(productTitle); if (isSingleDefaultVariant) { this.$container.find('.store-availabilities-modal__variant-title').remove(); } this.$container.find('.cc-popup').appendTo('body'); this.$modal = $('body').find('.store-availabilities-modal'); var popup = new ccPopup(this.$modal, this.namespace); this.$container.on('click', '[data-store-availability-modal-open]', () => { popup.open(); //When the modal is opened, try and get the users location this.functions.getUserLocation().then(coords => { if (coords && this.$modal.find('[data-distance="false"]').length) { //Re-retrieve the available stores location modal contents this.functions.getAvailableStores.call(this, variantId, response => { this.$modal.find('.store-availabilities-list').html($(response).find('.store-availabilities-list').html()); this.functions.updateLocationDistances.bind(this)(coords); }); } }); return false; }); this.$modal.on('click' + this.namespace, '.cc-popup-close, .cc-popup-background', () => { popup.close(); }); this.$container.removeClass(loadingClass); if (this.transitionDurationMS > 0) { var newHeight = this.$container.find('.store-availability-container').outerHeight(); this.$container.css('height', newHeight > 0 ? newHeight + 'px' : ''); clearTimeout(this.removeFixedHeightTimeout); this.removeFixedHeightTimeout = setTimeout(() => { this.$container.css('height', ''); }, this.transitionDurationMS); } } }); } } }; // Initialise the section when it's instantiated this.onSectionLoad(container); }; // Register section cc.sections.push({ name: 'store-availability', section: theme.StoreAvailability }); /** * Popup Section Script * ------------------------------------------------------------------------------ * * @namespace Popup */ theme.Popup = new function () { /** * Popup section constructor. Runs on page load as well as Theme Editor * `section:load` events. * @param {string} container - selector for the section container DOM element */ var dismissedStorageKey = 'cc-theme-popup-dismissed'; this.onSectionLoad = function (container) { this.namespace = theme.namespaceFromSection(container); this.$container = $(container); this.popup = new ccPopup(this.$container, this.namespace); var dismissForDays = this.$container.data('dismiss-for-days'), delaySeconds = this.$container.data('delay-seconds'), showPopup = true, testMode = this.$container.data('test-mode'), lastDismissed = window.localStorage.getItem(dismissedStorageKey); // Should we show it during this page view? // Check when it was last dismissed if (lastDismissed) { var dismissedDaysAgo = (new Date().getTime() - lastDismissed) / (1000 * 60 * 60 * 24); if (dismissedDaysAgo < dismissForDays) { showPopup = false; } } // Check for error or success messages if (this.$container.find('.cc-popup-form__response').length) { showPopup = true; delaySeconds = 1; // If success, set as dismissed if (this.$container.find('.cc-popup-form__response--success').length) { this.functions.popupSetAsDismissed.call(this); } } // Prevent popup on Shopify robot challenge page if (document.querySelector('.shopify-challenge__container')) { showPopup = false; } // Show popup, if appropriate if (showPopup || testMode) { setTimeout(() => { this.popup.open(); }, delaySeconds * 1000); } // Click on close button or modal background this.$container.on('click' + this.namespace, '.cc-popup-close, .cc-popup-background', () => { this.popup.close(() => { this.functions.popupSetAsDismissed.call(this); }); }); }; this.onSectionSelect = function () { this.popup.open(); }; this.functions = { /** * Use localStorage to set as dismissed */ popupSetAsDismissed: function popupSetAsDismissed() { window.localStorage.setItem(dismissedStorageKey, new Date().getTime()); } }; /** * Event callback for Theme Editor `section:unload` event */ this.onSectionUnload = function () { this.$container.off(this.namespace); }; }(); // Register section cc.sections.push({ name: 'newsletter-popup', section: theme.Popup }); /*================ General =================*/ theme.owlCarouselEventList = ['to', 'next', 'prev', 'destroy', 'initialized', 'resized', 'refreshed'].join('.owl.carousel ') + '.owl.carousel'; theme.beforeCarouselLoadEventBlockFix = function (carouselEls) { $(carouselEls).on(theme.owlCarouselEventList, function (evt) { evt.stopPropagation(); }); }; theme.beforeCarouselUnloadEventBlockFix = function (carouselEls) { $(carouselEls).off(theme.owlCarouselEventList); }; theme.loadCarousels = function (container) { return $('.carousel', container).each(function () { var $this = $(this); function getTotalProductBlockWidth() { var productBlockTotalWidth = 0; $this.find($this.hasClass('owl-loaded') ? '.owl-item:not(.cloned)' : '.product-block').each(function () { productBlockTotalWidth += $(this).outerWidth(true); }); return productBlockTotalWidth; } // next & prev arrows $this.closest('.collection-slider').on('click.themeCarousel', '.prev, .next', function (e) { e.preventDefault(); var carousel = $(this).closest('.collection-slider').find('.owl-carousel').data('owl.carousel'); if ($(this).hasClass('prev')) { carousel.prev(); } else { carousel.next(); } }); // create options var carouselOptions, isLooping; var fixedMode = !$(this).closest('.container--no-max').length; $this.toggleClass('carousel--fixed-grid-mode', fixedMode); if (fixedMode) { var desktopNumPerRow = parseInt($(this)[0].className.match(/per-row-(.)/)[1]); carouselOptions = { margin: 0, loop: false, autoWidth: false, items: 5, center: false, nav: false, dots: false, responsive: { 0: { items: desktopNumPerRow < 4 ? 1 : 2 }, 480: { items: Math.min(2, desktopNumPerRow - 2) }, 767: { items: desktopNumPerRow - 1 }, 1000: { items: desktopNumPerRow } } }; } else { isLooping = getTotalProductBlockWidth() > $this.width(); carouselOptions = { margin: 0, loop: isLooping, autoWidth: true, items: Math.min($this.children().length, 8), center: true, nav: false, dots: false }; } // init carousel var loadCarousel = function loadCarousel() { // remove data-src as not in correct format for Owl to parse $this.find('[data-src]').each(function () { $(this).attr('data-src-temp', $(this).attr('data-src')).removeAttr('data-src'); }); // fix for event propagation in nested owl carousels theme.beforeCarouselLoadEventBlockFix($this); // run after carousel is initialised $this.on('initialized.owl.carousel', function () { // restore data-src $this.find('[data-src-temp]').each(function () { $(this).attr('data-src', $(this).attr('data-src-temp')).removeAttr('data-src-temp'); }); // lazysizes on primary images $this.find('.product-block__image--primary .lazyload--manual, .product-block__image--secondary[data-image-index="1"] .lazyload--manual, .product-block-options__item.lazyload--manual').removeClass('lazyload--manual').addClass('lazyload'); // ensure clones are processed from scratch theme.ProductBlockManager.loadImages($this.closest('[data-section-type]')); // recalculate widths, after the above's async calls setTimeout(function () { $this.data('owl.carousel').invalidate('width'); $this.trigger('refresh.owl.carousel'); }, 10); }); // run after carousel is initialised or resized $this.on('initialized.owl.carousel resized.owl.carousel', function (evt) { // only loop if items do not all fit on screen, in non-fixed mode if (!fixedMode && evt.type == 'resized') { var shouldLoop = getTotalProductBlockWidth() > $this.width(); if (shouldLoop != isLooping) { // destroy and rebuild carousel with appropriate loop setting carouselOptions.loop = shouldLoop; isLooping = shouldLoop; // destroy function $(evt.target).data('owl.carousel').destroy(); // build again $(evt.target).owlCarousel(carouselOptions); // do no more in this callback return; } } // layout fixes setTimeout(function () { // fixes var currentWidth = $this.find('.owl-stage').width(); var stageIsPartiallyOffScreen = currentWidth > $this.width(); if (stageIsPartiallyOffScreen) { // more elements than fit in viewport // resize stage to avoid rounding-error wrapping var newWidth = 0; $this.find('.owl-item').each(function () { newWidth += $(this).outerWidth(true) + 1; }); $this.find('.owl-stage').css({}). removeClass('owl-stage--items-fit'); } else { // all elements fit inside viewport // centre-align using css, if not full $this.find('.owl-stage').addClass('owl-stage--items-fit').css({}); } // previous/next button visibility $this.closest('.collection-slider').find('.prev, .next').toggleClass('owl-btn-disabled', !stageIsPartiallyOffScreen); }, 10); }); // run when the contents change, for any reason theme.carouselInputIncrementer = theme.carouselInputIncrementer || 0; $this.on('refreshed.owl.carousel', function () { // set a11y attrs to avoid clone items giving duplication errors $('.owl-item', this).removeAttr('aria-hidden'); $('.owl-item.cloned', this).attr('aria-hidden', 'true').each(function () { theme.carouselInputIncrementer++; var uniquenessSuffix = '_' + theme.carouselInputIncrementer; $(this).find('form[id]:not([data-id-altered]), :input[id]:not([data-id-altered])').each(function () { $(this).attr('id', $(this).attr('id') + uniquenessSuffix); }).attr('data-id-altered', true); $(this).find('label[for]:not([data-id-altered])').each(function () { $(this).attr('for', $(this).attr('for') + uniquenessSuffix); }).attr('data-id-altered', true); }); }).addClass('owl-carousel').owlCarousel(carouselOptions); }; loadCarousel(); }); }; theme.unloadCarousels = function (container) { $('.collection-slider', container).off('.themeCarousel'); $('.slick-slider', container).slick('unslick'); theme.beforeCarouselUnloadEventBlockFix($('.owl-carousel', container)); $('.owl-carousel', container).each(function () { $(this).data('owl.carousel').destroy(); }); }; theme.icons = { left: '' + theme.strings.icon_labels_left + '', right: '' + theme.strings.icon_labels_right + '', close: '' + theme.strings.icon_labels_close + '', chevronLeft: '' + theme.strings.icon_labels_left + '', chevronRight: '' + theme.strings.icon_labels_right + '', chevronDown: '' + theme.strings.icon_labels_down + '', tick: '', label: 'Label' }; // Get Shopify feature support try { theme.Shopify.features = JSON.parse(document.documentElement.querySelector('#shopify-features').textContent); } catch (e) { theme.Shopify.features = {}; } theme.namespaceFromSection = function (container) { return ['.', $(container).data('section-type'), $(container).data('section-id')].join(''); }; theme.toggleLinkDropdownButton = function (evt) { evt.stopPropagation(); var $btn = $(evt.currentTarget), doExpand = $btn.attr('aria-expanded') == 'false'; $btn.attr('aria-expanded', doExpand); $btn.css('width', $btn.outerWidth() + 'px'); var newWidth = null, $optsBox = $btn.next(), isLeftAligned = $btn.closest('.link-dropdown').hasClass('link-dropdown--left-aligned'); if (!isLeftAligned) { if (doExpand) { newWidth = $optsBox.outerWidth(); newWidth += parseInt($optsBox.css('right')); newWidth -= parseInt($optsBox.find('.link-dropdown__link:first').css('padding-left')); } else { newWidth = parseInt($btn.css('padding-right')) + $btn.find('.link-dropdown__button-text').width() + 1; } setTimeout(function () { $btn.css('width', newWidth + 'px'); }, 10); } }; // Enables any images inside a container theme.awakenImagesFromSlumber = function ($cont) { $cont.find('.lazyload--manual:not(.lazyload)').addClass('lazyload'); }; theme.measureTextInsideElement = function (text, element) { theme.measureTextInsideElementCanvas = theme.measureTextInsideElementCanvas || document.createElement("canvas"); var context = theme.measureTextInsideElementCanvas.getContext("2d"); var font = getComputedStyle(element).font; context.font = font; return context.measureText(text); }; // Use non-breaking space to attempt to avoid orphans theme.avoidUnecessaryOrphans = function (element) { if (theme.settings.avoid_orphans) { var innerTextSplit = element.innerText.split(' '); if ( innerTextSplit.length >= 4 && // 4 or more words innerTextSplit[innerTextSplit.length - 1].length + innerTextSplit[innerTextSplit.length - 2].length < 20 // last two words aren't particularly long together ) { var html = element.innerHTML; if (html.indexOf(' ') < 0) { html = html.replace(/<[^>]*?>/g, match => { return match.replace(/ /g, 'ΩΩ'); }); var splitHtml = html.split(' '), lastItem = splitHtml.pop(); // ensure length of text will be smaller than container, excluding HTML if (theme.measureTextInsideElement(splitHtml[splitHtml.length - 1].replace(/<[^>]*?>/g, '') + ' ' + lastItem.replace(/<[^>]*?>/g, ''), element).width > element.clientWidth) { return; } html = splitHtml.join(' ') + ' ' + lastItem; html = html.replace(/ΩΩ/g, ' '); element.innerHTML = html; } } } }; theme.stickyHeaderHeight = function () { var v = getComputedStyle(document.documentElement).getPropertyValue('--theme-sticky-header-height'); if (v) { return parseInt(v) || 0; } else { return 0; } }; theme.scrollToRevealElement = function (el) { var elTop = el.getBoundingClientRect().top + window.scrollY, elBot = elTop + el.offsetHeight, inViewTop = window.scrollY, inViewBot = window.scrollY + window.innerHeight - 50; if (elTop < inViewTop || elBot > inViewBot) { window.scrollTo({ top: elTop - 100 - theme.stickyHeaderHeight(), left: window.screenLeft, behavior: 'smooth' }); } }; theme.getEmptyOptionSelectors = function ($formContainer) { var emptySections = []; $formContainer.find('[data-selector-type="dropdown"].option-selector').each(function () { if (!$(this).find('[aria-selected="true"]').filter(function () { return this.dataset && this.dataset.value; }).length) { emptySections.push(this); } }); $formContainer.find('[data-selector-type="listed"].option-selector').each(function () { if (!$(this).find('input:checked').length) { emptySections.push(this); } }); return emptySections; }; theme.validateProductForm = function ($formContainer) { $formContainer.find('.option-selector--empty'). removeClass('option-selector--empty'). find('.label__prefix'). remove(); if (!$formContainer.find('.original-selector').val()) { var emptySections = theme.getEmptyOptionSelectors($formContainer); if (emptySections.length) { emptySections.forEach(el => { el.classList.add('option-selector--empty'); var labelPrefix = document.createElement('span'); labelPrefix.innerText = theme.strings.products_product_pick_a + ' '; labelPrefix.className = 'label__prefix'; el.querySelector('.label').prepend(labelPrefix); }); theme.scrollToRevealElement(emptySections[0]); $formContainer.addClass('product-form--validation-started'); } return false; } else { return true; } }; // bg-set snippet in JS theme.lazyBGSetWidths = [180, 360, 540, 720, 900, 1080, 1296, 1512, 1728, 1950, 2100, 2260, 2450, 2700, 3000, 3350, 3750, 4100]; theme.loadLazyBGSet = function (container) { $('[data-lazy-bgset-src]', container).each(function () { var srcset = '', masterSrc = $(this).data('lazy-bgset-src'), aspectRatio = $(this).data('lazy-bgset-aspect-ratio'), masterWidth = $(this).data('lazy-bgset-width'), masterHeight = Math.round(masterWidth / aspectRatio); for (var i = 0; i < theme.lazyBGSetWidths.length; i++) { var thisWidth = theme.lazyBGSetWidths[i]; if (masterWidth >= thisWidth) { srcset += "".concat(theme.Shopify.formatImage(masterSrc, thisWidth + 'x'), " ").concat(thisWidth, "w ").concat(Math.round(thisWidth / aspectRatio), "h, "); } } srcset += "".concat(masterSrc, " ").concat(masterWidth, "w ").concat(masterHeight, "h"); $(this).attr('data-bgset', srcset).removeClass('lazyload--manual').addClass('lazyload'); }); }; /*=============== Components ===============*/ theme.applyAjaxToProductForm = function ($formContainer) { $formContainer.find('form.product-purchase-form').each(function () { var $form = $(this); $form.on('submit', function (evt) { // Validate if (!theme.validateProductForm($formContainer)) { return false; } // Ajax-add if (theme.settings.cart_type === 'drawer' && $formContainer.is('[data-ajax-add-to-cart="true"]')) { evt.preventDefault(); var cartPopupTemplate = [ '
    ', '
    ', '
    ', '
    ', theme.strings.products_added_notification_title, '
    ', '
    ', '
    ', '
    ', theme.strings.products_added_notification_item, '
    ', '
    ', '
    ', '
    ', '
    ', theme.strings.products_added_notification_unit_price, '
    ', '
    ', '
    ', '
    ', theme.strings.products_added_notification_quantity, '
    ', '
    ', '
    ', '
    ', theme.strings.products_added_notification_total_price, '
    ', '
    ', '
    ', '
    ', '
    ', theme.strings.products_added_notification_subtotal, '
    ', '
    ', '
    ', '
    ', '
    ', '[[encoded-title]]', '
    ', '
    ', '
    [[title]]
    ', '
    [[variants]]
    ', '
    ', '
    [[unit_price]]
    ', '
    ', theme.strings.products_added_notification_quantity, '[[quantity]]', '
    ', '
    [[line_price]]
    ', '
    ', '
    ', '
    ', '
    ', '[[unit_price]]', '
    ', '
    ', '[[quantity]]', '
    ', '
    ', '
    [[line_price]]
    ', '
    ', '[[line_discount]]', '
    ', '
    ', '[[subtotal]]', '
    ', theme.strings.products_added_notification_shipping_note, ' 
    ', '
    ', '
    ', '', '
    ']. join(''); var shopifyAjaxAddURL = theme.routes.cart_add_url + '.js'; var shopifyAjaxCartURL = theme.routes.cart_url + '.js'; //Disable add button $form.find('button[type="submit"]').attr('disabled', 'disabled').each(function () { $(this).data('previous-value', $(this).val()); }).val(theme.strings.products_product_adding_to_cart); //Hide any existing notifications $('#cart-summary-overlay #shop-more').triggerHandler('click'); //Add to cart $.post(shopifyAjaxAddURL, $form.serialize(), function (itemData) { //Dispatch change event document.documentElement.dispatchEvent( new CustomEvent('theme:cartchanged', { bubbles: true, cancelable: false })); //Enable add button $form.find('button[type="submit"]').removeAttr('disabled').each(function () { $btn = $(this); //Set to 'DONE', alter button style, wait a few secs, revert to normal $btn.val(theme.strings.products_product_added_to_cart).addClass('inverted'); window.setTimeout(function () { $btn.removeClass('inverted').val($btn.data('previous-value')); }, 3000); }); //Get our data var addedDataJSON = $.parseJSON(itemData); //Get current cart state $.get(shopifyAjaxCartURL, function (cartData) { var cartDataJSON = $.parseJSON(cartData), addedQty = addedDataJSON.quantity, addedImage = addedDataJSON.image, productData = theme.OptionManager.getProductData($form), originalVariantPrice = addedDataJSON.original_price; // The only way to get the compare at price is from the in-page JSON dump for (var i = 0; i < productData.variants.length; i++) { var variantData = productData.variants[i]; if (variantData.id == addedDataJSON.variant_id && variantData.compare_at_price && variantData.compare_at_price > originalVariantPrice) { originalVariantPrice = variantData.compare_at_price; } } // Variants require option names, which are not included in the cart var variantHtml = ''; if (addedDataJSON.variant_title) {// catches default variant for (var i = 0; i < productData.options.length; i++) { variantHtml += [ '
    ', '', productData.options[i].name, '', '', addedDataJSON.variant_options[i], '', '
    '].join(''); } } if (addedDataJSON.selling_plan_allocation && addedDataJSON.selling_plan_allocation.selling_plan.name) { variantHtml += [ '
    ', '', addedDataJSON.selling_plan_allocation.selling_plan.name, '', '
    '].join(''); } if ($form.data('show-preorder-label')) { variantHtml += "
    ".concat(theme.strings.products_product_preorder, "
    "); } else if ($form.data('show-sale-price-label') && originalVariantPrice > addedDataJSON.original_price) { variantHtml += "
    ".concat(theme.strings.products_labels_sale, "
    "); } var unitPriceHtml = ''; if (originalVariantPrice > addedDataJSON.final_price) { unitPriceHtml += '
    ' + theme.Shopify.formatMoney(originalVariantPrice, theme.money_format) + '
    '; } unitPriceHtml += '
    ' + theme.Shopify.formatMoney(addedDataJSON.final_price, theme.money_format) + '
    '; unitPriceHtml += theme.renderUnitPrice(addedDataJSON.unit_price, addedDataJSON.unit_price_measurement, theme.money_format); var linePriceHtml = ''; if (originalVariantPrice != addedDataJSON.final_price) { linePriceHtml += '
    ' + theme.Shopify.formatMoney(originalVariantPrice * addedQty, theme.money_format) + '
    '; linePriceHtml += '
    ' + theme.Shopify.formatMoney(addedDataJSON.final_line_price, theme.money_format) + '
    '; } else { linePriceHtml += '' + theme.Shopify.formatMoney(addedDataJSON.final_line_price, theme.money_format) + ''; } var lineDiscountHtml = ''; if (addedDataJSON.line_level_discount_allocations && addedDataJSON.line_level_discount_allocations.length > 0) { lineDiscountHtml += '
      '; for (var i = 0; i < addedDataJSON.line_level_discount_allocations.length; i++) { var discount_allocation = addedDataJSON.line_level_discount_allocations[i]; lineDiscountHtml += [ '
    • ', '', '', theme.icons.label, '', '', discount_allocation.discount_application.title, '', '', '', theme.Shopify.formatMoney(discount_allocation.amount, theme.money_format), '', '
    • '].join(''); } lineDiscountHtml += '
    '; } var subtotalHtml = ''; if (cartDataJSON.cart_level_discount_applications && cartDataJSON.cart_level_discount_applications.length > 0) { subtotalHtml += '
      '; for (var i = 0; i < cartDataJSON.cart_level_discount_applications.length; i++) { var discount_application = cartDataJSON.cart_level_discount_applications[i]; subtotalHtml += [ '
    • ', '', '', theme.icons.label, '', '', discount_application.title, '', '', '', theme.Shopify.formatMoney(discount_application.total_allocated_amount, theme.money_format), '', '
    • '].join(''); } subtotalHtml += '
    '; } subtotalHtml += '' + theme.strings.products_added_notification_subtotal + ': '; subtotalHtml += '' + theme.Shopify.formatMoney(cartDataJSON.total_price, theme.money_format_with_cart_code_preference) + ''; //Now we have all the data, build the shade var cartShadeHTML = cartPopupTemplate; cartShadeHTML = cartShadeHTML.replace('[[title]]', addedDataJSON.product_title); cartShadeHTML = cartShadeHTML.replace('[[encoded-title]]', addedDataJSON.product_title.replace(/"/g, '"').replace(/&/g, '&')); cartShadeHTML = cartShadeHTML.replace('[[variants]]', variantHtml); cartShadeHTML = cartShadeHTML.replace(/\[\[quantity\]\]/g, addedQty); cartShadeHTML = cartShadeHTML.replace('[[image_url]]', theme.Shopify.formatImage(addedImage, '170x')); cartShadeHTML = cartShadeHTML.split('[[unit_price]]').join(unitPriceHtml); cartShadeHTML = cartShadeHTML.split('[[line_price]]').join(linePriceHtml); cartShadeHTML = cartShadeHTML.split('[[line_discount]]').join(lineDiscountHtml); cartShadeHTML = cartShadeHTML.replace('[[subtotal]]', subtotalHtml); var $cartShade = $(cartShadeHTML); $cartShade.find('#shop-more').bind('click', function () { $cartShade.animate({ top: -$cartShade.outerHeight() }, 500, function () { $(this).remove(); }); return false; }); $cartShade.prependTo('body').css('top', -$cartShade.outerHeight()).animate({ top: 0 }, 500); }, 'html'); window.location.href=Shopify.routes.client_routes.cart_url+window.location.search; }, 'text').fail(function (data) { //Enable add button $form.find('button[type="submit"]').removeAttr('disabled').each(function () { $(this).val($(this).data('previous-value')); }); //Not added, show message if (typeof data != 'undefined' && typeof data.status != 'undefined') { var jsonRes = $.parseJSON(data.responseText); theme.showQuickPopup(jsonRes.description, $form.find('button[type="submit"]:first')); } else { //Some unknown error? Disable ajax and add the old-fashioned way. $formContainer.attr('ajax-add-to-cart', 'false'); $form.submit(); } }); } }); }); if (theme.settings.cart_type === 'drawer') { $(window).off('.ajaxAddScroll').on('scroll.ajaxAddScroll', function () { // Hide notifications on scroll $('#cart-summary-overlay #shop-more').triggerHandler('click'); }); } }; theme.removeAjaxFromProductForm = function ($formContainer) { $formContainer.find('form.product-purchase-form').off('submit'); }; theme.LoadFilterer = function (section) { this.$container = section.$container; this.namespace = section.namespace; this.registerEventListener = section.registerEventListener; this.functions = { initFiltersEtc: function initFiltersEtc() { $('.filter-container', this.$container).addClass('filter-container--mobile-initialised'); // append query vars onto sort urls (e.g. filters, vendor collection) if (location.href.indexOf('?') >= 0) { $('#sort-dropdown-options .link-dropdown__link', this.$container).each(function () { var queryTerms = location.href.split('?')[1].split('&'); var newHref = $(this).attr('href'); queryTerms.forEach(term => { if (term.indexOf('sort_by=') === -1) { newHref += '&' + term; } }); $(this).attr('href', newHref); }); } theme.ProductBlockManager.loadImages(this.$container); }, switchGridLayout: function switchGridLayout(evt) { evt.preventDefault(); if ($(evt.currentTarget).hasClass('layout-switch--one-column')) { this.$container.find('.product-list').removeClass('product-list--per-row-mob-2').addClass('product-list--per-row-mob-1'); } else { this.$container.find('.product-list').removeClass('product-list--per-row-mob-1').addClass('product-list--per-row-mob-2'); } $(evt.currentTarget).addClass('layout-switch--active').siblings().removeClass('layout-switch--active'); }, checkStickyScroll: function checkStickyScroll() { var utilityBarOffsetY = $('.utility-bar').offset().top; if ($(window).width() < 768 && this.previousScrollTop > window.scrollY && window.scrollY > utilityBarOffsetY) { $('body').addClass('utility-bar-sticky-mobile-copy-reveal'); } else { $('body.utility-bar-sticky-mobile-copy-reveal').removeClass('utility-bar-sticky-mobile-copy-reveal'); } this.previousScrollTop = window.scrollY; }, ajaxLoadLink: function ajaxLoadLink(evt) { evt.preventDefault(); this.functions.ajaxLoadUrl.call(this, $(evt.currentTarget).attr('href')); }, ajaxLoadForm: function ajaxLoadForm(evt) { if (evt.type === 'submit') { evt.preventDefault(); } var queryVals = []; evt.currentTarget.querySelectorAll('input, select').forEach(input => { if ( (input.type !== 'checkbox' && input.type !== 'radio' || input.checked) && // is an active input value input.value !== '' // has a value ) { queryVals.push([input.name, encodeURIComponent(input.value)]); } }); // new url var newUrl = location.pathname; queryVals.forEach(value => { newUrl += "&".concat(value[0], "=").concat(value[1]); }); newUrl = newUrl.replace('&', '?'); // load this.functions.ajaxLoadUrl.call(this, newUrl); }, ajaxPopState: function ajaxPopState(event) { this.functions.ajaxLoadUrl.call(this, document.location.href); }, ajaxLoadUrl: function ajaxLoadUrl(url) { // update url history var fullUrl = url; if (fullUrl.slice(0, 1) === '/') { fullUrl = window.location.protocol + '//' + window.location.host + fullUrl; } window.history.pushState({ path: fullUrl }, '', fullUrl); // start fetching URL var refreshContainerSelector = '[data-ajax-container]', $ajaxContainers = this.$container.find(refreshContainerSelector); // loading state $ajaxContainers.addClass('ajax-loading'); // fetch content if (this.currentAjaxLoadUrlFetch) { this.currentAjaxLoadUrlFetch.abort(); } this.currentAjaxLoadUrlFetch = $.get(url, function (data) { this.currentAjaxLoadUrlFetch = null; // save active element if (document.activeElement) { this.activeElementId = document.activeElement.id; } // unload content theme.ProductBlockManager.unloadImages(this.$container); // replace contents var $newAjaxContainers = $("
    ".concat(data, "
    ")).find(refreshContainerSelector); $newAjaxContainers.each(function (index) { $($ajaxContainers[index]).html($(this).html()); }); // init js this.functions.initFiltersEtc.call(this); // init theme components var $componentHaver = this.$container.closest('[data-components]'); if ($componentHaver.length) { var components = $componentHaver.data('components').split(','); components.forEach(function (component) { $(document).trigger('cc:component:load', [component, $componentHaver[0]]); }.bind(this)); } // remove loading state $ajaxContainers.removeClass('ajax-loading'); // init scroll animations theme.initAnimateOnScroll(); // restore active element if (this.activeElementId) { var el = document.getElementById(this.activeElementId); if (el) { el.focus(); } } // scroll viewport (must be done after any page size changes) var scrollToY = $('[data-ajax-scroll-to]:first', this.$container).offset().top - $('.section-header').height(); window.scrollTo({ top: scrollToY, behavior: "smooth" }); }.bind(this)); } }; // ajax filter & sort if (this.$container.data('ajax-filtering')) { // ajax load on link click this.$container.on('click' + this.namespace, '.link-dropdown__link, .filter-group__applied-item, .filter-group__clear-link, .pagination a', this.functions.ajaxLoadLink.bind(this)); // ajax load form submission this.$container.on('change' + this.namespace + ' submit' + this.namespace, '#CollectionFilterForm', theme.debounce(this.functions.ajaxLoadForm.bind(this), 700)); // handle back button this.registerEventListener(window, 'popstate', this.functions.ajaxPopState.bind(this)); } else { this.$container.on('change' + this.namespace, '#CollectionFilterForm', function () { $(this).submit(); }); } // sort dropdown: open this.$container.on('click' + this.namespace, '.link-dropdown__button', theme.toggleLinkDropdownButton.bind(this)); // sort dropdown: click anywhere else in page to close $(document).on('click' + this.namespace, function () { $('.link-dropdown__button[aria-expanded="true"]').trigger('click'); }); if (document.querySelector('.utility-bar')) { // duplicate utility bar for mobile var $utilBarClone = $('.utility-bar').clone().addClass('utility-bar--sticky-mobile-copy').removeAttr('data-ajax-container').insertAfter('.utility-bar'); // ensure unique ids theme.suffixIds($utilBarClone[0], 'dupe'); this.previousScrollTop = window.scrollY; $(window).on('throttled-scroll' + this.namespace, this.functions.checkStickyScroll.bind(this)); // filter visibility this.$container.on('click' + this.namespace, '[data-toggle-filters]', function (evt) { var isNowVisible = $('.filter-container', this.$container).toggleClass('filter-container--show-filters-desktop filter-container--show-filters-mobile').hasClass('filter-container--show-filters-desktop'); $('.toggle-btn[data-toggle-filters]', this.$container).toggleClass('toggle-btn--revealed-desktop', isNowVisible); // handle resized collection grid theme.ProductBlockManager.afterImagesResized(); $('.slick-slider', this.$container).each(function () { $(this).slick('setPosition'); }); return false; }.bind(this)); // layout switcher this.$container.on('click' + this.namespace, '.layout-switch', this.functions.switchGridLayout.bind(this)); } // init things that may need re-initialising on ajax load this.functions.initFiltersEtc.call(this); this.destroy = function () { theme.ProductBlockManager.unloadImages(this.$container); theme.destroyProductGallery(this.$container); this.$container.off(this.namespace); $(window).off(this.namespace); $(document).off(this.namespace); }.bind(this); }; theme.buildGalleryViewer = function (config) { // create viewer var $allContainer = $('