window.theme = window.theme || {}; window.slate = window.slate || {}; /* ================ SLATE ================ */ theme.Sections = function Sections() { this.constructors = {}; this.instances = []; }; theme.Sections.prototype = _.assignIn({}, theme.Sections.prototype, { _createInstance: function(container, constructor) { var $container = $(container); var id = $container.attr('data-section-id'); var type = $container.attr('data-section-type'); constructor = constructor || this.constructors[type]; if (_.isUndefined(constructor)) { return; } var instance = _.assignIn(new constructor(container), { id: id, type: type, container: container }); this.instances.push(instance); }, _onSectionLoad: function(evt) { var container = $('[data-section-id]', evt.target)[0]; if (container) { this._createInstance(container); } }, _onSectionUnload: function(evt) { this.instances = _.filter(this.instances, function(instance) { var isEventInstance = instance.id === evt.detail.sectionId; if (isEventInstance) { if (_.isFunction(instance.onUnload)) { instance.onUnload(evt); } } return !isEventInstance; }); }, _onSelect: function(evt) { // eslint-disable-next-line no-shadow var instance = _.find(this.instances, function(instance) { return instance.id === evt.detail.sectionId; }); if (!_.isUndefined(instance) && _.isFunction(instance.onSelect)) { instance.onSelect(evt); } }, _onDeselect: function(evt) { // eslint-disable-next-line no-shadow var instance = _.find(this.instances, function(instance) { return instance.id === evt.detail.sectionId; }); if (!_.isUndefined(instance) && _.isFunction(instance.onDeselect)) { instance.onDeselect(evt); } }, _onBlockSelect: function(evt) { // eslint-disable-next-line no-shadow var instance = _.find(this.instances, function(instance) { return instance.id === evt.detail.sectionId; }); if (!_.isUndefined(instance) && _.isFunction(instance.onBlockSelect)) { instance.onBlockSelect(evt); } }, _onBlockDeselect: function(evt) { // eslint-disable-next-line no-shadow var instance = _.find(this.instances, function(instance) { return instance.id === evt.detail.sectionId; }); if (!_.isUndefined(instance) && _.isFunction(instance.onBlockDeselect)) { instance.onBlockDeselect(evt); } }, register: function(type, constructor) { this.constructors[type] = constructor; $('[data-section-type=' + type + ']').each( function(index, container) { this._createInstance(container, constructor); }.bind(this) ); } }); window.slate = window.slate || {}; /** * iFrames * ----------------------------------------------------------------------------- * Wrap videos in div to force responsive layout. * * @namespace iframes */ slate.rte = { wrapTable: function() { $('.rte table').wrap('
'); }, iframeReset: function() { var $iframeVideo = $( '.rte iframe[src*="youtube.com/embed"], .rte iframe[src*="player.vimeo"]' ); var $iframeReset = $iframeVideo.add('.rte iframe#admin_bar_iframe'); $iframeVideo.each(function() { // Add wrapper to make video responsive $(this).wrap(''); }); $iframeReset.each(function() { // Re-set the src attribute on each iframe after page load // for Chrome's "incorrect iFrame content on 'back'" bug. // https://code.google.com/p/chromium/issues/detail?id=395791 // Need to specifically target video and admin bar this.src = this.src; }); } }; window.slate = window.slate || {}; /** * A11y Helpers * ----------------------------------------------------------------------------- * A collection of useful functions that help make your theme more accessible * to users with visual impairments. * * * @namespace a11y */ slate.a11y = { /** * For use when focus shifts to a container rather than a link * eg for In-page links, after scroll, focus shifts to content area so that * next `tab` is where user expects if focusing a link, just $link.focus(); * * @param {JQuery} $element - The element to be acted upon */ pageLinkFocus: function($element) { var focusClass = 'js-focus-hidden'; $element .first() .attr('tabIndex', '-1') .focus() .addClass(focusClass) .one('blur', callback); function callback() { $element .first() .removeClass(focusClass) .removeAttr('tabindex'); } }, /** * If there's a hash in the url, focus the appropriate element */ focusHash: function() { var hash = window.location.hash; // is there a hash in the url? is it an element on the page? if (hash && document.getElementById(hash.slice(1))) { this.pageLinkFocus($(hash)); } }, /** * When an in-page (url w/hash) link is clicked, focus the appropriate element */ bindInPageLinks: function() { $('a[href*=#]').on( 'click', function(evt) { this.pageLinkFocus($(evt.currentTarget.hash)); }.bind(this) ); }, /** * Traps the focus in a particular container * * @param {object} options - Options to be used * @param {jQuery} options.$container - Container to trap focus within * @param {jQuery} options.$elementToFocus - Element to be focused when focus leaves container * @param {string} options.namespace - Namespace used for new focus event handler */ trapFocus: function(options) { var eventName = options.namespace ? 'focusin.' + options.namespace : 'focusin'; if (!options.$elementToFocus) { options.$elementToFocus = options.$container; } options.$container.attr('tabindex', '-1'); options.$elementToFocus.focus(); $(document).on(eventName, function(evt) { if ( options.$container[0] !== evt.target && !options.$container.has(evt.target).length ) { options.$container.focus(); } }); }, /** * Removes the trap of focus in a particular container * * @param {object} options - Options to be used * @param {jQuery} options.$container - Container to trap focus within * @param {string} options.namespace - Namespace used for new focus event handler */ removeTrapFocus: function(options) { var eventName = options.namespace ? 'focusin.' + options.namespace : 'focusin'; if (options.$container && options.$container.length) { options.$container.removeAttr('tabindex'); } $(document).off(eventName); } }; /** * Currency Helpers * ----------------------------------------------------------------------------- * A collection of useful functions that help with currency formatting * * Current contents * - formatMoney - Takes an amount in cents and returns it as a formatted dollar value. * * Alternatives * - Accounting.js - http://openexchangerates.github.io/accounting.js/ * */ theme.Currency = (function() { var moneyFormat = '${{amount}}'; // eslint-disable-line camelcase function formatMoney(cents, format) { if (typeof cents === 'string') { cents = cents.replace('.', ''); } var value = ''; var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/; var formatString = format || moneyFormat; function formatWithDelimiters(number, precision, thousands, decimal) { thousands = thousands || ','; decimal = decimal || '.'; if (isNaN(number) || number === null) { return 0; } number = (number / 100.0).toFixed(precision); var parts = number.split('.'); var dollarsAmount = parts[0].replace( /(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + thousands ); var centsAmount = parts[1] ? decimal + parts[1] : ''; return dollarsAmount + centsAmount; } switch (formatString.match(placeholderRegex)[1]) { case 'amount': value = formatWithDelimiters(cents, 2); break; case 'amount_no_decimals': value = formatWithDelimiters(cents, 0); break; case 'amount_with_comma_separator': value = formatWithDelimiters(cents, 2, '.', ','); break; case 'amount_no_decimals_with_comma_separator': value = formatWithDelimiters(cents, 0, '.', ','); break; case 'amount_no_decimals_with_space_separator': value = formatWithDelimiters(cents, 0, ' '); break; case 'amount_with_apostrophe_separator': value = formatWithDelimiters(cents, 2, "'"); break; } return formatString.replace(placeholderRegex, value); } return { formatMoney: formatMoney }; })(); theme.Images = (function() { function preload(images, size) { if (typeof images === 'string') { images = [images]; } for (var i = 0; i < images.length; i++) { var image = images[i]; this.loadImage(this.getSizedImageUrl(image, size)); } } function loadImage(path) { new Image().src = path; } /** * Swaps the src of an image for another OR returns the imageURL to the callback function * @param image * @param element * @param callback */ function switchImage(image, element, callback) { var size = this.imageSize(element.src); var imageUrl = this.getSizedImageUrl(image.src, size); if (callback) { callback(imageUrl, image, element); // eslint-disable-line callback-return } else { element.src = imageUrl; } } function imageSize(src) { src = src || ''; var match = src.match( /.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\\.@]/ ); if (match === null) { return null; } else { return match[1]; } } function getSizedImageUrl(src, size) { if (size === null) { return src; } if (size === 'master') { return this.removeProtocol(src); } var match = src.match( /\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i ); if (match !== null) { var prefix = src.split(match[0]); var suffix = match[0]; return this.removeProtocol(prefix[0] + '_' + size + suffix); } return null; } function removeProtocol(path) { return path.replace(/http(s)?:/, ''); } return { preload: preload, loadImage: loadImage, switchImage: switchImage, imageSize: imageSize, getSizedImageUrl: getSizedImageUrl, removeProtocol: removeProtocol }; })(); /** * Variant Selection scripts * ------------------------------------------------------------------------------ * * Handles change events from the variant inputs in any `cart/add` forms that may * exist. Also updates the master select and triggers updates when the variants * price or image changes. * * @namespace variants */ slate.Variants = (function() { /** * Variant constructor * * @param {object} options - Settings from `product.js` */ function Variants(options) { this.$container = options.$container; this.product = options.product; this.singleOptionSelector = options.singleOptionSelector; this.originalSelectorId = options.originalSelectorId; this.enableHistoryState = options.enableHistoryState; this.currentVariant = this._getVariantFromOptions(); $(this.singleOptionSelector, this.$container).on( 'change', this._onSelectChange.bind(this) ); } Variants.prototype = _.assignIn({}, Variants.prototype, { /** * Get the currently selected options from add-to-cart form. Works with all * form input elements. * * @return {array} options - Values of currently selected variants */ _getCurrentOptions: function() { var currentOptions = _.map( $(this.singleOptionSelector, this.$container), function(element) { var $element = $(element); var type = $element.attr('type'); var currentOption = {}; if (type === 'radio' || type === 'checkbox') { if ($element[0].checked) { currentOption.value = $element.val(); currentOption.index = $element.data('index'); return currentOption; } else { return false; } } else { currentOption.value = $element.val(); currentOption.index = $element.data('index'); return currentOption; } } ); // remove any unchecked input values if using radio buttons or checkboxes currentOptions = _.compact(currentOptions); return currentOptions; }, /** * Find variant based on selected values. * * @param {array} selectedValues - Values of variant inputs * @return {object || undefined} found - Variant object from product.variants */ _getVariantFromOptions: function() { var selectedValues = this._getCurrentOptions(); var variants = this.product.variants; var found = _.find(variants, function(variant) { return selectedValues.every(function(values) { return _.isEqual(variant[values.index], values.value); }); }); return found; }, /** * Event handler for when a variant input changes. */ _onSelectChange: function() { var variant = this._getVariantFromOptions(); this.$container.trigger({ type: 'variantChange', variant: variant }); if (!variant) { return; } this._updateMasterSelect(variant); this._updateImages(variant); this._updatePrice(variant); this._updateSKU(variant); this.currentVariant = variant; if (this.enableHistoryState) { this._updateHistoryState(variant); } }, /** * Trigger event when variant image changes * * @param {object} variant - Currently selected variant * @return {event} variantImageChange */ _updateImages: function(variant) { var variantImage = variant.featured_image || {}; var currentVariantImage = this.currentVariant.featured_image || {}; if ( !variant.featured_image || variantImage.src === currentVariantImage.src ) { return; } this.$container.trigger({ type: 'variantImageChange', variant: variant }); }, /** * Trigger event when variant price changes. * * @param {object} variant - Currently selected variant * @return {event} variantPriceChange */ _updatePrice: function(variant) { if ( variant.price === this.currentVariant.price && variant.compare_at_price === this.currentVariant.compare_at_price ) { return; } this.$container.trigger({ type: 'variantPriceChange', variant: variant }); }, /** * Trigger event when variant sku changes. * * @param {object} variant - Currently selected variant * @return {event} variantSKUChange */ _updateSKU: function(variant) { if (variant.sku === this.currentVariant.sku) { return; } this.$container.trigger({ type: 'variantSKUChange', variant: variant }); }, /** * Update history state for product deeplinking * * @param {variant} variant - Currently selected variant * @return {k} [description] */ _updateHistoryState: function(variant) { if (!history.replaceState || !variant) { return; } var newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?variant=' + variant.id; window.history.replaceState({ path: newurl }, '', newurl); }, /** * Update hidden master select of variant change * * @param {variant} variant - Currently selected variant */ _updateMasterSelect: function(variant) { $(this.originalSelectorId, this.$container).val(variant.id); } }); return Variants; })(); /*================ MODULES ================*/ window.Drawers = (function() { var Drawer = function(id, position, options) { var defaults = { close: '.js-drawer-close', open: '.js-drawer-open-' + position, openClass: 'js-drawer-open', dirOpenClass: 'js-drawer-open-' + position }; this.nodes = { $parent: $('body, html'), $page: $('.page-element'), $moved: $('.is-moved-by-drawer') }; this.config = $.extend(defaults, options); this.position = position; this.$drawer = $('#' + id); this.$open = $(this.config.open); if (!this.$drawer.length) { return false; } this.drawerIsOpen = false; this.init(); }; Drawer.prototype.init = function() { this.$open.attr('aria-expanded', 'false'); this.$open.on('click', $.proxy(this.open, this)); this.$drawer.find(this.config.close).on('click', $.proxy(this.close, this)); }; Drawer.prototype.open = function(evt) { // Keep track if drawer was opened from a click, or called by another function var externalCall = false; // don't open an opened drawer if (this.drawerIsOpen) { return; } this.$open.addClass(this.config.openClass); // Prevent following href if link is clicked if (evt) { evt.preventDefault(); } else { externalCall = true; } // Without this, the drawer opens, the click event bubbles up to $nodes.page // which closes the drawer. if (evt && evt.stopPropagation) { evt.stopPropagation(); // save the source of the click, we'll focus to this on close this.$activeSource = $(evt.currentTarget); } if (this.drawerIsOpen && !externalCall) { return this.close(); } // Add is-transitioning class to moved elements on open so drawer can have // transition for close animation this.nodes.$moved.addClass('is-transitioning'); this.$drawer.prepareTransition(); this.nodes.$parent.addClass( this.config.openClass + ' ' + this.config.dirOpenClass ); this.drawerIsOpen = true; // Set focus on drawer slate.a11y.trapFocus({ $container: this.$drawer, namespace: 'drawer_focus' }); // Run function when draw opens if set if ( this.config.onDrawerOpen && typeof this.config.onDrawerOpen === 'function' ) { if (!externalCall) { this.config.onDrawerOpen(); } } if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { this.$activeSource.attr('aria-expanded', 'true'); } this.bindEvents(); }; Drawer.prototype.close = function() { // don't close a closed drawer if (!this.drawerIsOpen) { return; } this.$open.removeClass(this.config.openClass); // deselect any focused form elements $(document.activeElement).trigger('blur'); // Ensure closing transition is applied to moved elements, like the nav this.nodes.$moved.prepareTransition({ disableExisting: true }); this.$drawer.prepareTransition({ disableExisting: true }); this.nodes.$parent.removeClass( this.config.dirOpenClass + ' ' + this.config.openClass ); this.drawerIsOpen = false; // Remove focus on drawer slate.a11y.removeTrapFocus({ $container: this.$drawer, namespace: 'drawer_focus' }); if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { this.$activeSource.attr('aria-expanded', 'false'); } this.unbindEvents(); }; Drawer.prototype.bindEvents = function() { // Lock scrolling on mobile this.nodes.$page.on('touchmove.drawer', function() { return false; }); // Clicking out of drawer closes it this.nodes.$page.on( 'click.drawer', $.proxy(function() { this.close(); return false; }, this) ); // Pressing escape closes drawer this.nodes.$parent.on( 'keyup.drawer', $.proxy(function(evt) { if (evt.keyCode === 27) { this.close(); } }, this) ); }; Drawer.prototype.unbindEvents = function() { this.nodes.$page.off('.drawer'); this.nodes.$parent.off('.drawer'); }; return Drawer; })(); theme.Hero = (function() { var selectors = { hero: '.hero', heroWrapper: '.hero-wrapper', heroArrows: '.hero__arrow', heroImage: '.hero__image', heroPause: '.hero__pause' }; function Hero() { this.namespace = '.hero'; this.$hero = $(selectors.hero); this.$hero.on('init' + this.namespace, this._a11y.bind(this)); this.$hero.on('init' + this.namespace, this._arrowsInit.bind(this)); this.$hero.slick({ accessibility: true, arrows: false, // this theme has custom arrows draggable: false, autoplay: this.$hero.data('autoplay'), autoplaySpeed: this.$hero.data('speed') }); $(selectors.heroImage).on( 'click' + this.namespace, function() { this.$hero.slick('slickNext'); }.bind(this) ); $(selectors.heroPause).on( 'click' + this.namespace, function() { if ($(selectors.heroPause).hasClass('is-paused')) { this._play(); } else { this._pause(); } }.bind(this) ); } Hero.prototype = _.assignIn({}, Hero.prototype, { _pause: function() { this.$hero.slick('slickPause'); $(selectors.heroPause).addClass('is-paused'); }, _play: function() { this.$hero.slick('slickPlay'); $(selectors.heroPause).removeClass('is-paused'); }, _a11y: function(event, obj) { var $list = obj.$list; // Remove default Slick aria-live attr until slider is focused $list.removeAttr('aria-live'); // When an element in the slider is focused // pause slideshow and set aria-live $(selectors.heroWrapper).on( 'focusin' + this.namespace, function(evt) { if (!$(selectors.heroWrapper).has(evt.target).length) { return; } $list.attr('aria-live', 'polite'); this._pause(); }.bind(this) ); // Resume autoplay $(selectors.heroWrapper).on( 'focusout' + this.namespace, function(evt) { if (!$(selectors.heroWrapper).has(evt.target).length) { return; } $list.removeAttr('aria-live'); this._play(); }.bind(this) ); }, _arrowsInit: function(event, obj) { // Slider is initialized. Setup custom arrows var count = obj.slideCount; var $slider = obj.$slider; var $arrows = $(selectors.heroArrows); if ($arrows.length && count > 1) { $arrows.on( 'click' + this.namespace, function(evt) { evt.preventDefault(); if ($(evt.currentTarget).hasClass('hero__arrow--prev')) { $slider.slick('slickPrev'); } else { $slider.slick('slickNext'); } this._scrollTop(); }.bind(this) ); } else { $arrows.remove(); } }, _scrollTop: function() { var currentScroll = $(document).scrollTop(); var heroOffset = this.$hero.offset().top; if (currentScroll > heroOffset) { $('html') .add('body') .animate( { scrollTop: heroOffset }, 250 ); } }, goToSlide: function(slideIndex) { this.$hero.slick('slickGoTo', slideIndex); }, pause: function() { this.$hero.slick('slickPause'); }, play: function() { this.$hero.slick('slickPlay'); }, destroy: function() { this.$hero.off(this.namespace); $(selectors.heroImage).off(this.namespace); $(selectors.heroPause).off(this.namespace); $(selectors.heroWrapper).off(this.namespace); $(selectors.heroArrows).off(this.namespace); this.$hero.slick('unslick'); } }); return Hero; })(); window.Modals = (function() { var Modal = function(id, name, options) { var defaults = { close: '.js-modal-close', open: '.js-modal-open-' + name, openClass: 'modal--is-active' }; this.$modal = $('#' + id); if (!this.$modal.length) { return false; } this.nodes = { $body: $('body') }; this.config = $.extend(defaults, options); this.modalIsOpen = false; this.$focusOnOpen = this.config.focusOnOpen ? $(this.config.focusOnOpen) : this.$modal; this.init(); }; Modal.prototype.init = function() { var $openBtn = $(this.config.open); // Add aria controls $openBtn.attr('aria-expanded', 'false'); $(this.config.open).on('click', $.proxy(this.open, this)); this.$modal.find(this.config.close).on('click', $.proxy(this.close, this)); }; Modal.prototype.open = function(evt) { // Keep track if modal was opened from a click, or called by another function var externalCall = false; // don't open an opened modal if (this.modalIsOpen) { return; } // Prevent following href if link is clicked if (evt) { evt.preventDefault(); } else { externalCall = true; } // Without this, the modal opens, the click event bubbles up to $nodes.page // which closes the modal. if (evt && evt.stopPropagation) { evt.stopPropagation(); // save the source of the click, we'll focus to this on close this.$activeSource = $(evt.currentTarget); } if (this.modalIsOpen && !externalCall) { return this.close(); } this.$modal.prepareTransition().addClass(this.config.openClass); this.nodes.$body.addClass(this.config.openClass); this.modalIsOpen = true; // Set focus on modal slate.a11y.trapFocus({ $container: this.$modal, namespace: 'modal_focus', $elementToFocus: this.$focusOnOpen }); if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { this.$activeSource.attr('aria-expanded', 'true'); } this.bindEvents(); }; Modal.prototype.close = function() { // don't close a closed modal if (!this.modalIsOpen) { return; } // deselect any focused form elements $(document.activeElement).trigger('blur'); this.$modal.prepareTransition().removeClass(this.config.openClass); this.nodes.$body.removeClass(this.config.openClass); this.modalIsOpen = false; // Remove focus on modal slate.a11y.removeTrapFocus({ $container: this.$modal, namespace: 'modal_focus' }); if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { this.$activeSource.attr('aria-expanded', 'false').focus(); } this.unbindEvents(); }; Modal.prototype.bindEvents = function() { // Pressing escape closes modal this.nodes.$body.on( 'keyup.modal', $.proxy(function(evt) { if (evt.keyCode === 27) { this.close(); } }, this) ); }; Modal.prototype.unbindEvents = function() { this.nodes.$body.off('.modal'); }; return Modal; })(); window.Meganav = (function() { var Meganav = function(options) { this.cache = { $document: $(document), $page: $('.page-element') }; var defaults = { $meganavs: $('.meganav'), $megaNav: $('.meganav__nav'), $meganavToggle: $('.meganav-toggle'), $meganavDropdownContainers: $('.site-nav__dropdown-container'), $meganavToggleThirdLevel: $('.meganav__link-toggle'), $meganavLinkSecondLevel: $('.meganav__link--second-level'), $meganavLinkThirdLevel: $('.meganav__link--third-level'), $meganavDropdownThirdLevel: $('.site-nav__dropdown--third-level'), isOpen: false, preventDuplicates: false, closeOnPageClick: false, closeThirdLevelOnBlur: false, activeClass: 'meganav--active', drawerClass: 'meganav--drawer', meganavDropdown: '.site-nav__dropdown', meganavLinkClass: 'meganav__link', drawerToggleClass: 'drawer__nav-toggle-btn', drawerNavItem: '.drawer__nav-item', navCollectionClass: 'meganav__nav--collection', secondLevelClass: 'meganav__link--second-level', thirdLevelClass: 'meganav__link-toggle', thirdLevelContainerClass: 'site-nav__dropdown--third-level', noAnimationClass: 'meganav--no-animation' }; this.config = $.extend(defaults, options); this.init(); }; Meganav.prototype.init = function() { var $openBtn = this.config.$meganavToggle; $openBtn.on('click', $.proxy(this.requestMeganav, this)); if (this.config.closeThirdLevelOnBlur) { this.config.$meganavLinkThirdLevel.on( 'blur', $.proxy(this.closeThirdLevelMenu, this) ); } }; Meganav.prototype.requestMeganav = function(evt) { var $targetedMeganav; // Prevent following href if link is clicked if (evt) { evt.preventDefault(); } // Without this, the meganav opens, the click event bubbles up to // theme.cache.$page which closes the drawer. if (evt && evt.stopPropagation) { evt.stopPropagation(); } var $el = $(evt.currentTarget); var anotherNavIsOpen = this.config.isOpen; var isThirdLevelBtn = $el.hasClass(this.config.thirdLevelClass); // The $targetedMeganav is different for the drawer and non-drawer navs if ($el.hasClass(this.config.drawerToggleClass)) { $targetedMeganav = $el .closest(this.config.drawerNavItem) .children('.' + this.config.drawerClass); } else { $targetedMeganav = $el.siblings(this.config.meganavDropdown); } // Navigate to link href or close menu if already active if ($el.hasClass(this.config.activeClass) && $el.is('a')) { window.location = $el.attr('href'); return; } // If true, don't let multiple meganavs be open simultaneously if (!isThirdLevelBtn && this.config.preventDuplicates) { this.close(); } if ($targetedMeganav.hasClass(this.config.drawerClass)) { var isExpanded = $el.attr('aria-expanded') === 'true'; $el .toggleClass(this.config.activeClass) .attr('aria-expanded', !isExpanded); $targetedMeganav.stop().slideToggle(200); } else { $el.addClass(this.config.activeClass).attr('aria-expanded', 'true'); // Show targeted nav, letting it know whether another meganav is already open this.open($el, $targetedMeganav, anotherNavIsOpen); } // Setup event handlers when meganav is open this.bindEvents(); this.config.isOpen = true; // If dropdown has third level, calculate width for container var $dropdown = $el.next(); var isCollection = $dropdown .find(this.config.$megaNav) .hasClass(this.config.navCollectionClass); if (isCollection) { this.updateThirdLevelContainerWidth($el, $dropdown); } }; Meganav.prototype.updateThirdLevelContainerWidth = function($el, $dropdown) { var $thirdLevel = $dropdown.find(this.config.$meganavDropdownThirdLevel); if (!$thirdLevel.length) { return; } $.each( $thirdLevel, function(key, container) { var $container = $(container); var $lastChild = $container.find('li:last-child'); this.updateContainerWidth($container, $lastChild); }.bind(this) ); }; Meganav.prototype.updateContainerWidth = function(container, element) { var containerRect = container[0].getBoundingClientRect(); var elementRect = element[0].getBoundingClientRect(); if (elementRect.left < containerRect.right) { return; } var columnWidth = containerRect.width; var containerFixedWidth = elementRect.left + columnWidth - containerRect.left; var numberOfColumns = containerFixedWidth / columnWidth; var containerPercentageWidth = numberOfColumns * 20; container .width(containerPercentageWidth + '%') .find('li') .css('width', 100 / numberOfColumns + '%'); }; Meganav.prototype.open = function($el, $target, noAnimation) { var isThirdLevelBtn = $el.hasClass(this.config.thirdLevelClass); $target.addClass(this.config.activeClass); if (isThirdLevelBtn) { this.toggleSubNav($el, $target); } // Add class to override animation - CSS determined if (noAnimation) { $target.addClass(this.config.noAnimationClass); } }; Meganav.prototype.toggleSubNav = function($el) { this.removeMenuActiveState(); $el .addClass(this.config.activeClass) .attr('aria-expanded', 'true') .siblings(this.config.$meganavDropdownThirdLevel) .addClass(this.config.activeClass); $el.parent().addClass(this.config.activeClass); }; Meganav.prototype.close = function(evt, $target) { if (this.config.preventDuplicates) { // Close all meganavs this.config.$meganavs.removeClass( [this.config.activeClass, this.config.noAnimationClass].join(' ') ); this.config.$meganavToggle .removeClass(this.config.activeClass) .attr('aria-expanded', 'false'); this.config.$meganavDropdownContainers.removeClass( this.config.activeClass ); } else { // Close targeted nav var $targetedMeganav = $('#' + $target.attr('aria-controls')); $targetedMeganav.removeClass( [this.config.activeClass, this.config.noAnimationClass].join(' ') ); $target .removeClass(this.config.activeClass) .attr('aria-expanded', 'false'); } // Remove event listeners this.unbindEvents(); this.config.isOpen = false; }; Meganav.prototype.closeThirdLevelMenu = function(evt) { var $el = $(evt.currentTarget); var $parent = $el.parent(); if (!$parent.is(':last-child')) { return; } this.config.$meganavLinkSecondLevel.one( 'focus.meganav', $.proxy(function() { this.removeMenuActiveState(); }, this) ); }; Meganav.prototype.removeMenuActiveState = function() { var activeClasses = [this.config.activeClass, this.config.noAnimationClass]; this.config.$meganavToggleThirdLevel .removeClass(activeClasses.join(' ')) .attr('aria-expanded', 'false'); this.config.$meganavDropdownThirdLevel.removeClass(activeClasses.join(' ')); this.config.$meganavDropdownContainers.removeClass(this.config.activeClass); }; Meganav.prototype.bindEvents = function() { if (!this.config.closeOnPageClick) { return; } // Clicking away from the meganav will close it this.cache.$page.on('click.meganav', $.proxy(this.close, this)); // Exception to above: clicking anywhere on the meganav will NOT close it this.config.$meganavs.on( 'click.meganav', function(evt) { // 3rd level container var is3rdLevelMenuTarget = $(evt.currentTarget).hasClass(this.config.activeClass) && $(evt.currentTarget).hasClass(this.config.thirdLevelContainerClass); // 2nd level mega link var isMegaNavlink = $(evt.target).hasClass(this.config.meganavLinkClass) && $(evt.target).hasClass(this.config.secondLevelClass); // If we click anything outside from the 3rd level megaNav, close the third level menu (except for 2nd level links) if (!is3rdLevelMenuTarget && !isMegaNavlink) { this.removeMenuActiveState(); } evt.stopImmediatePropagation(); }.bind(this) ); // Pressing escape closes meganav and focuses the target parent link this.cache.$document.on( 'keyup.meganav', $.proxy(function(evt) { if (evt.keyCode !== 27) return; this.config.$meganavToggle .filter('.' + this.config.activeClass) .focus(); this.close(); }, this) ); }; Meganav.prototype.unbindEvents = function() { if (!this.config.closeOnPageClick) { return; } this.cache.$page.off('.meganav'); this.config.$meganavs.off('.meganav'); this.cache.$document.off('.meganav'); this.config.$meganavLinkSecondLevel.off('.meganav'); this.config.$meganavLinkThirdLevel.off('.meganav'); }; return Meganav; })(); window.QtySelector = (function() { var QtySelector = function($el) { this.cache = { $body: $('body'), $subtotal: $('#CartSubtotal'), $discountTotal: $('#cartDiscountTotal'), $cartTable: $('.cart-table'), $cartTemplate: $('#CartProducts') }; this.settings = { loadingClass: 'js-qty--is-loading', isCartTemplate: this.cache.$body.hasClass('template-cart'), // On the cart template, minimum is 0. Elsewhere min is 1 minQty: this.cache.$body.hasClass('template-cart') ? 0 : 1 }; this.$el = $el; this.qtyUpdateTimeout; this.createInputs(); this.bindEvents(); }; QtySelector.prototype.createInputs = function() { var $el = this.$el; var data = { value: $el.val(), key: $el.attr('id'), name: $el.attr('name'), line: $el.attr('data-line') }; var source = $('#QuantityTemplate').html(); var template = Handlebars.compile(source); this.$wrapper = $(template(data)).insertBefore($el); // Remove original number input $el.remove(); }; QtySelector.prototype.validateAvailability = function(line, quantity) { var product = theme.cartObject.items[line - 1]; // 0-based index in API var handle = product.handle; // needed for the ajax request var id = product.id; // needed to find right variant from ajax results var params = { type: 'GET', url: '/products/' + handle + '.js', dataType: 'json', success: $.proxy(function(cartProduct) { this.validateAvailabilityCallback(line, quantity, id, cartProduct); }, this) }; $.ajax(params); }; QtySelector.prototype.validateAvailabilityCallback = function( line, quantity, id, product ) { var quantityIsAvailable = true; // This returns all variants of a product. // Loop through them to get our desired one. for (var i = 0; i < product.variants.length; i++) { var variant = product.variants[i]; if (variant.id === id) { break; } } // If the variant tracks inventory and does not sell when sold out // we can compare the requested with available quantity if ( variant.inventory_management !== null && variant.inventory_policy === 'deny' ) { if (variant.inventory_quantity < quantity) { // Show notification with error message theme.Notify.open('error', theme.strings.noStockAvailable, true); // Set quantity to max amount available this.$wrapper.find('.js-qty__input').val(variant.inventory_quantity); quantityIsAvailable = false; this.$wrapper.removeClass(this.settings.loadingClass); } } if (quantityIsAvailable) { this.updateItemQuantity(line, quantity); } }; QtySelector.prototype.validateQty = function(qty) { if (parseFloat(qty) === parseInt(qty, 10) && !isNaN(qty)) { // We have a valid number! } else { // Not a number. Default to 1. qty = 1; } return parseInt(qty, 10); }; QtySelector.prototype.adjustQty = function(evt) { var $el = $(evt.currentTarget); var $input = $el.siblings('.js-qty__input'); var qty = this.validateQty($input.val()); var line = $input.attr('data-line'); if ($el.hasClass('js-qty__adjust--minus')) { qty -= 1; if (qty <= this.settings.minQty) { qty = this.settings.minQty; } } else { qty += 1; } if (this.settings.isCartTemplate) { $el.parent().addClass(this.settings.loadingClass); this.updateCartItemPrice(line, qty); } else { $input.val(qty); } }; QtySelector.prototype.bindEvents = function() { this.$wrapper .find('.js-qty__adjust') .on('click', $.proxy(this.adjustQty, this)); // Select input text on click this.$wrapper.on('click', '.js-qty__input', function() { this.setSelectionRange(0, this.value.length); }); // If the quantity changes on the cart template, update the price if (this.settings.isCartTemplate) { this.$wrapper.on( 'change', '.js-qty__input', $.proxy(function(evt) { var $input = $(evt.currentTarget); var line = $input.attr('data-line'); var qty = this.validateQty($input.val()); $input.parent().addClass(this.settings.loadingClass); this.updateCartItemPrice(line, qty); }, this) ); } }; return QtySelector; })(); /* Allow product to be added to cart via ajax with custom success and error responses. */ window.AjaxCart = (function() { var cart = function($form) { this.cache = { $cartIconIndicator: $('.site-header__cart-indicator') }; this.$form = $form; this.eventListeners(); }; cart.prototype.eventListeners = function() { if (this.$form.length) { this.$form.on('submit', $.proxy(this.addItemFromForm, this)); } }; cart.prototype.addItemFromForm = function(evt) { evt.preventDefault(); if(window.BOLD && BOLD.helpers && typeof BOLD.helpers.addItemFromForm === 'function'){ BOLD.helpers.addItemFromForm(this.$form, $.proxy(function(line_item) { this.success(line_item); }, this), $.proxy(function(XMLHttpRequest, textStatus) { this.error(XMLHttpRequest, textStatus); }, this)); return; } var params = { type: 'POST', url: '/cart/add.js', data: this.$form.serialize(), dataType: 'json', success: $.proxy(function(lineItem) { this.success(lineItem); }, this), error: $.proxy(function(XMLHttpRequest, textStatus) { this.error(XMLHttpRequest, textStatus); }, this) }; $.ajax(params); }; cart.prototype.success = function() { theme.Notify.open('success', false, true); // Update cart notification bubble's state this.cache.$cartIconIndicator.removeClass('hide'); }; cart.prototype.error = function(XMLHttpRequest) { var data = JSON.parse(XMLHttpRequest.responseText); if (data.message) { theme.Notify.open('error', data.description, true); } }; return cart; })(); window.Notify = (function() { var notify = function() { this.cache = { $scrollParent: $('html').add('body'), $notificationSuccess: $('#NotificationSuccess'), $notificationSuccessLink: $('#NotificationSuccess').find('a'), $notificationError: $('#NotificationError'), $notificationPromo: $('#NotificationPromo'), $notificationClose: $('.notification__close'), $notificationErrorMessage: $('.notification__message--error') }; this.settings = { notifyActiveClass: 'notification--active', closeTimer: 5000, promoKeyName: 'promo-' + this.cache.$notificationPromo.data('text') }; this.notifyTimer; this.$lastFocusedElement = null; this.isLocalStorageSupported = isLocalStorageSupported(); this.cache.$notificationClose.on('click', this.close.bind(this)); this.showPromo(); }; function isLocalStorageSupported() { // Return false if we are in an iframe without access to sessionStorage if (window.self !== window.top) { return false; } var testKey = 'test'; try { var storage = window.sessionStorage; storage.setItem(testKey, '1'); storage.removeItem(testKey); return true; } catch (error) { return false; } } notify.prototype.open = function(state, message, autoclose) { this.close(); if (state === 'success') { this.cache.$notificationSuccess .attr('aria-hidden', false) .addClass(this.settings.notifyActiveClass); // Set last focused element to return to once success // notification is dismissed (by timeout) this.$lastFocusedElement = $(document.activeElement); // Set focus on link to cart after transition this.cache.$notificationSuccess.one( 'TransitionEnd webkitTransitionEnd transitionend oTransitionEnd', $.proxy(function() { slate.a11y.pageLinkFocus(this.cache.$notificationSuccessLink); }, this) ); // Fallback for no transitions if (this.cache.$scrollParent.hasClass('no-csstransitions')) { slate.a11y.pageLinkFocus(this.cache.$notificationSuccessLink); } } else { this.cache.$notificationErrorMessage.html(message); this.cache.$notificationError .attr('aria-hidden', false) .addClass(this.settings.notifyActiveClass); } // Timeout to close the notification if (autoclose) { clearTimeout(this.notifyTimer); this.notifyTimer = setTimeout( this.close.bind(this), this.settings.closeTimer ); } }; notify.prototype.close = function(evt) { if (evt && $(evt.currentTarget).attr('id') === 'NotificationPromoClose') { if (this.isLocalStorageSupported) { localStorage.setItem(this.settings.promoKeyName, 'hidden'); } } // Return focus to previous element if one is defined // and the user has not strayed from the success notification link if ( this.$lastFocusedElement && this.cache.$notificationSuccessLink.is(':focus') ) { slate.a11y.pageLinkFocus(this.$lastFocusedElement); } // Close all notification bars this.cache.$notificationSuccess .attr('aria-hidden', true) .removeClass(this.settings.notifyActiveClass); this.cache.$notificationError .attr('aria-hidden', true) .removeClass(this.settings.notifyActiveClass); this.cache.$notificationPromo .attr('aria-hidden', true) .removeClass(this.settings.notifyActiveClass); // Reset last focused element this.$lastFocusedElement = null; }; notify.prototype.showPromo = function(SFEevent) { // If reloaded in the storefront editor, update selectors/settings if (SFEevent) { this.initCache(); } if ( this.isLocalStorageSupported && localStorage[this.settings.promoKeyName] === 'hidden' ) { return; } this.cache.$notificationPromo .attr('aria-hidden', false) .addClass(this.settings.notifyActiveClass); }; return notify; })(); theme.Maps = (function() { var config = { zoom: 14, styles: [ { featureType: 'water', elementType: 'geometry', stylers: [{ color: '#cacaca' }, { lightness: 17 }] }, { featureType: 'landscape', elementType: 'geometry', stylers: [{ color: '#e1e1e1' }, { lightness: 20 }] }, { featureType: 'road.highway', elementType: 'geometry.fill', stylers: [{ color: '#ffffff' }, { lightness: 17 }] }, { featureType: 'road.highway', elementType: 'geometry.stroke', stylers: [{ color: '#ffffff' }, { lightness: 29 }, { weight: 0.2 }] }, { featureType: 'road.arterial', elementType: 'geometry', stylers: [{ color: '#ffffff' }, { lightness: 18 }] }, { featureType: 'road.local', elementType: 'geometry', stylers: [{ color: '#ffffff' }, { lightness: 16 }] }, { featureType: 'poi', elementType: 'geometry', stylers: [{ color: '#e1e1e1' }, { lightness: 21 }] }, { featureType: 'poi.park', elementType: 'geometry', stylers: [{ color: '#bbbbbb' }, { lightness: 21 }] }, { elementType: 'labels.text.stroke', stylers: [{ visibility: 'on' }, { color: '#ffffff' }, { lightness: 16 }] }, { elementType: 'labels.text.fill', stylers: [{ saturation: 36 }, { color: '#333333' }, { lightness: 40 }] }, { elementType: 'labels.icon', stylers: [{ visibility: 'off' }] }, { featureType: 'transit', elementType: 'geometry', stylers: [{ color: '#f2f2f2' }, { lightness: 19 }] }, { featureType: 'administrative', elementType: 'geometry.fill', stylers: [{ color: '#fefefe' }, { lightness: 20 }] }, { featureType: 'administrative', elementType: 'geometry.stroke', stylers: [{ color: '#fefefe' }, { lightness: 17 }, { weight: 1.2 }] } ] // eslint-disable-line key-spacing, comma-spacing }; var apiStatus = null; var mapsToLoad = []; function Map(container) { theme.$currentMapContainer = this.$container = $(container); var key = this.$container.data('api-key'); if (typeof key !== 'string' || key === '') { return; } if (apiStatus === 'loaded') { var self = this; // Check if the script has previously been loaded with this key var $script = $('script[src*="' + key + '&"]'); if ($script.length === 0) { $.getScript( 'https://maps.googleapis.com/maps/api/js?key=' + key ).then(function() { apiStatus = 'loaded'; self.createMap(); }); } else { this.createMap(); } } else { mapsToLoad.push(this); if (apiStatus !== 'loading') { apiStatus = 'loading'; if (typeof window.google === 'undefined') { $.getScript( 'https://maps.googleapis.com/maps/api/js?key=' + key ).then(function() { apiStatus = 'loaded'; initAllMaps(); }); } } } } function initAllMaps() { // API has loaded, load all Map instances in queue $.each(mapsToLoad, function(index, instance) { instance.createMap(); }); } function geolocate($map) { var deferred = $.Deferred(); var geocoder = new google.maps.Geocoder(); var address = $map.data('address-setting'); geocoder.geocode({ address: address }, function(results, status) { if (status !== google.maps.GeocoderStatus.OK) { deferred.reject(status); } deferred.resolve(results); }); return deferred; } Map.prototype = _.assignIn({}, Map.prototype, { createMap: function() { var $map = this.$container.find('.map-section__container'); return geolocate($map) .then( function(results) { var mapOptions = { zoom: config.zoom, styles: config.styles, center: results[0].geometry.location, draggable: false, clickableIcons: false, scrollwheel: false, disableDoubleClickZoom: true, disableDefaultUI: true }; var map = (this.map = new google.maps.Map($map[0], mapOptions)); var center = (this.center = map.getCenter()); var enablePin = $map.data('enable-pin'); var markerColor = $map.data('marker-color'); var markerIcon = { path: 'M57.7,0C25.8,0,0,25.8,0,57.7C0,89.5,50,170,57.7,170s57.7-80.5,57.7-112.3C115.3,25.8,89.5,0,57.7,0z M57.7,85 c-14.9,0-27-12.1-27-27c0-14.9,12.1-27,27-27c14.9,0,27,12.1,27,27C84.7,72.9,72.6,85,57.7,85z', fillColor: markerColor, fillOpacity: 0.9, scale: 0.2, strokeWeight: 0 }; //eslint-disable-next-line no-unused-vars var marker = new google.maps.Marker({ map: map, position: center, icon: markerIcon, visible: enablePin }); google.maps.event.addDomListener( window, 'resize', $.debounce(250, function() { google.maps.event.trigger(map, 'resize'); map.setCenter(center); }) ); }.bind(this) ) .fail(function() { var errorMessage; switch (status) { case 'ZERO_RESULTS': errorMessage = theme.strings.addressNoResults; break; case 'OVER_QUERY_LIMIT': errorMessage = theme.strings.addressQueryLimit; break; default: errorMessage = theme.strings.addressError; break; } // Only show error in the theme editor if (Stacker.designMode) { var $mapContainer = $map.parents('.map-section'); $mapContainer.addClass('page-width map-section--load-error'); $mapContainer.find('.map-section__content-wrapper').remove(); $mapContainer .find('.map-section__wrapper') .html( '