· 6 years ago · Feb 16, 2020, 11:52 AM
1// Use our custom event listeners to tap into common functions.
2// Documentation - https://archetypethemes.co/blogs/streamline/javascript-events-for-developers
3//
4// document.addEventListener('page:loaded', function() {
5// /* Stylesheet and theme scripts have loaded */
6// });
7
8window.theme = window.theme || {};
9window.slate = window.slate || {};
10
11if (console && console.log) {
12 console.log('Streamline theme ('+theme.settings.themeVersion+') by ARCHΞTYPE | Learn more at https://archetypethemes.co');
13}
14
15window.lazySizesConfig = window.lazySizesConfig || {};
16lazySizesConfig.expFactor = 4;
17
18(function($){
19 var $ = jQuery = $;
20
21 slate.utils = {
22 /**
23 * _.defaultTo from lodash
24 * Checks `value` to determine whether a default value should be returned in
25 * its place. The `defaultValue` is returned if `value` is `NaN`, `null`,
26 * or `undefined`.
27 * Source: https://github.com/lodash/lodash/blob/master/defaultTo.js
28 *
29 * @param {*} value - Value to check
30 * @param {*} defaultValue - Default value
31 * @returns {*} - Returns the resolved value
32 */
33 defaultTo: function(value, defaultValue) {
34 return (value == null || value !== value) ? defaultValue : value
35 },
36
37 promiseStylesheet: function() {
38 if (typeof this.stylesheetPromise === 'undefined') {
39 this.stylesheetPromise = $.Deferred(function(defer) {
40 var link = document.querySelector('link[href="' + theme.stylesheet + '"]');
41
42 if (link.loaded) {
43 defer.resolve();
44 }
45
46 onloadCSS(link, function() { // Global onloadCSS function injected by load-css.liquid
47 defer.resolve();
48 });
49 });
50 }
51
52 return this.stylesheetPromise;
53 }
54 };
55
56 slate.a11y = {
57
58 /**
59 * Traps the focus in a particular container
60 *
61 * @param {object} options - Options to be used
62 * @param {jQuery} options.$container - Container to trap focus within
63 * @param {jQuery} options.$elementToFocus - Element to be focused when focus leaves container
64 * @param {string} options.namespace - Namespace used for new focus event handler
65 */
66 trapFocus: function(options) {
67 var eventName = options.namespace
68 ? 'focusin.' + options.namespace
69 : 'focusin';
70
71 if (!options.$elementToFocus) {
72 options.$elementToFocus = options.$container;
73 }
74
75 options.$container.attr('tabindex', '-1');
76 options.$elementToFocus.focus();
77
78 $(document).off('focusin');
79
80 $(document).on(eventName, function(evt) {
81 if (options.$container[0] !== evt.target && !options.$container.has(evt.target).length) {
82 options.$container.focus();
83 }
84 });
85 },
86
87 /**
88 * Removes the trap of focus in a particular container
89 *
90 * @param {object} options - Options to be used
91 * @param {jQuery} options.$container - Container to trap focus within
92 * @param {string} options.namespace - Namespace used for new focus event handler
93 */
94 removeTrapFocus: function(options) {
95 var eventName = options.namespace
96 ? 'focusin.' + options.namespace
97 : 'focusin';
98
99 if (options.$container && options.$container.length) {
100 options.$container.removeAttr('tabindex');
101 }
102
103 $(document).off(eventName);
104 },
105
106 // Not from Slate, but fit in the a11y category
107 lockMobileScrolling: function(namespace, $element) {
108 if ($element) {
109 var $el = $element;
110 } else {
111 var $el = $(document.documentElement).add('body');
112 }
113 $el.on('touchmove' + namespace, function () {
114 return false;
115 });
116 },
117
118 unlockMobileScrolling: function(namespace, $element) {
119 if ($element) {
120 var $el = $element;
121 } else {
122 var $el = $(document.documentElement).add('body');
123 }
124 $el.off(namespace);
125 },
126
127 promiseAnimationEnd: function($el) {
128 var events = 'animationend webkitAnimationEnd oAnimationEnd';
129 var properties = ['animation-duration', '-moz-animation-duration', '-webkit-animation-duration', '-o-animation-duration'];
130 var duration = 0;
131 var promise = $.Deferred().resolve();
132
133 // check the various CSS properties to see if a duration has been set
134 $.each(properties, function(index, value) {
135 duration || (duration = parseFloat($el.css(value)));
136 });
137
138 if (duration > 0) {
139 promise = $.Deferred(function(defer) {
140 $el.on(events, function(evt) {
141 if (evt.target !== $el[0]) return;
142 $el.off(events);
143 defer.resolve();
144 });
145 });
146 }
147
148 return promise;
149 },
150
151 promiseTransitionEnd: function($el) {
152 var events = 'webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend';
153 var properties = ['transition-duration', '-moz-transition-duration', '-webkit-transition-duration', '-o-transition-duration'];
154 var duration = 0;
155 var promise = $.Deferred().resolve();
156
157 // check the various CSS properties to see if a duration has been set
158 $.each(properties, function(index, value) {
159 duration || (duration = parseFloat($el.css(value)));
160 });
161
162 if (duration > 0) {
163 promise = $.Deferred(function(defer) {
164 $el.on(events, function(evt) {
165 if (evt.target !== $el[0]) return;
166 $el.off(events);
167 defer.resolve();
168 });
169 });
170 }
171
172 return promise;
173 }
174 };
175
176 theme.Sections = function Sections() {
177 this.constructors = {};
178 this.instances = [];
179
180 $(document)
181 .on('shopify:section:load', this._onSectionLoad.bind(this))
182 .on('shopify:section:unload', this._onSectionUnload.bind(this))
183 .on('shopify:section:select', this._onSelect.bind(this))
184 .on('shopify:section:deselect', this._onDeselect.bind(this))
185 .on('shopify:block:select', this._onBlockSelect.bind(this))
186 .on('shopify:block:deselect', this._onBlockDeselect.bind(this));
187 };
188
189 theme.Sections.prototype = $.extend({}, theme.Sections.prototype, {
190 createInstance: function(container, constructor, customScope) {
191 var $container = $(container);
192 var id = $container.attr('data-section-id');
193 var type = $container.attr('data-section-type');
194
195 constructor = constructor || this.constructors[type];
196
197 if (typeof constructor === 'undefined') {
198 return;
199 }
200
201 // If custom scope passed, check to see if instance
202 // is already initialized so we don't double up
203 if (customScope) {
204 var instanceExists = this._findInstance(id);
205 if (instanceExists) {
206 return;
207 }
208 }
209
210 var instance = $.extend(new constructor(container), {
211 id: id,
212 type: type,
213 container: container,
214 namespace: '.' + type + '-' + id
215 });
216
217 this.instances.push(instance);
218 },
219
220 _onSectionLoad: function(evt, subSection, subSectionId) {
221 if (AOS) {
222 AOS.refreshHard();
223 }
224
225 var container = subSection ? subSection : $('[data-section-id]', evt.target)[0];
226
227 if (!container) {
228 return;
229 }
230
231 this.createInstance(container);
232
233 var instance = subSection ? subSectionId : this._findInstance(evt.detail.sectionId);
234
235 if (!subSection) {
236 this.loadSubSections();
237 }
238
239 // Run JS only in case of the section being selected in the editor
240 // before merchant clicks "Add"
241 if (instance && typeof instance.onLoad === 'function') {
242 instance.onLoad(evt);
243 }
244 },
245
246 loadSubSections: function($context) {
247 var $sections = $context ? $context.find('[data-subsection]') : $('[data-subsection]');
248
249 $sections.each(function(evt, el) {
250 this._onSectionLoad(null, el, $(el).data('section-id'));
251 }.bind(this));
252
253 if (AOS) {
254 AOS.refreshHard();
255 }
256 },
257
258 _onSectionUnload: function(evt) {
259 var instance = this._removeInstance(evt.detail.sectionId);
260 if (instance && typeof instance.onUnload === 'function') {
261 instance.onUnload(evt);
262 }
263 },
264
265 _onSelect: function(evt) {
266 var instance = this._findInstance(evt.detail.sectionId);
267
268 if (instance && typeof instance.onSelect === 'function') {
269 instance.onSelect(evt);
270 }
271 },
272
273 _onDeselect: function(evt) {
274 var instance = this._findInstance(evt.detail.sectionId);
275
276 if (instance && typeof instance.onDeselect === 'function') {
277 instance.onDeselect(evt);
278 }
279 },
280
281 _onBlockSelect: function(evt) {
282 var instance = this._findInstance(evt.detail.sectionId);
283
284 if (instance && typeof instance.onBlockSelect === 'function') {
285 instance.onBlockSelect(evt);
286 }
287 },
288
289 _onBlockDeselect: function(evt) {
290 var instance = this._findInstance(evt.detail.sectionId);
291
292 if (instance && typeof instance.onBlockDeselect === 'function') {
293 instance.onBlockDeselect(evt);
294 }
295 },
296
297 _findInstance: function(id) {
298 for (var i = 0; i < this.instances.length; i++) {
299 if (this.instances[i].id === id) {
300 return this.instances[i];
301 }
302 }
303 },
304
305 _removeInstance: function(id) {
306 var i = this.instances.length;
307 var instance;
308
309 while(i--) {
310 if (this.instances[i].id === id) {
311 instance = this.instances[i];
312 this.instances.splice(i, 1);
313 break;
314 }
315 }
316
317 return instance;
318 },
319
320 reinitSection: function(section) {
321 for (var i = 0; i < sections.instances.length; i++) {
322 var instance = sections.instances[i];
323 if (instance['type'] === section) {
324 if (typeof instance.forceReload === 'function') {
325 instance.forceReload();
326 }
327 }
328 }
329 },
330
331 register: function(type, constructor, $scope) {
332 var afterLoad = false;
333 this.constructors[type] = constructor;
334 var $sections = $('[data-section-type=' + type + ']');
335
336 // Any section within the scope
337 if ($scope) {
338 $sections = $('[data-section-type=' + type + ']', $scope);
339 }
340
341 $sections.each(function(index, container) {
342 this.createInstance(container, constructor, $scope);
343 }.bind(this));
344 }
345 });
346
347 theme.Currency = (function() {
348 var moneyFormat = '${{amount}}';
349
350 function formatMoney(cents, format) {
351 if (typeof cents === 'string') {
352 cents = cents.replace('.', '');
353 }
354 var value = '';
355 var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/;
356 var formatString = (format || moneyFormat);
357
358 function formatWithDelimiters(number, precision, thousands, decimal) {
359 precision = slate.utils.defaultTo(precision, 2);
360 thousands = slate.utils.defaultTo(thousands, ',');
361 decimal = slate.utils.defaultTo(decimal, '.');
362
363 if (isNaN(number) || number == null) {
364 return 0;
365 }
366
367 number = (number / 100.0).toFixed(precision);
368
369 var parts = number.split('.');
370 var dollarsAmount = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + thousands);
371 var centsAmount = parts[1] ? (decimal + parts[1]) : '';
372
373 return dollarsAmount + centsAmount;
374 }
375
376 switch (formatString.match(placeholderRegex)[1]) {
377 case 'amount':
378 value = formatWithDelimiters(cents, 2);
379 break;
380 case 'amount_no_decimals':
381 value = formatWithDelimiters(cents, 0);
382 break;
383 case 'amount_with_comma_separator':
384 value = formatWithDelimiters(cents, 2, '.', ',');
385 break;
386 case 'amount_no_decimals_with_comma_separator':
387 value = formatWithDelimiters(cents, 0, '.', ',');
388 break;
389 case 'amount_no_decimals_with_space_separator':
390 value = formatWithDelimiters(cents, 0, ' ');
391 break;
392 }
393
394 return formatString.replace(placeholderRegex, value);
395 }
396
397 function getBaseUnit(variant) {
398 if (!variant) {
399 return;
400 }
401
402 if (!variant.unit_price_measurement || !variant.unit_price_measurement.reference_value) {
403 return;
404 }
405
406 return variant.unit_price_measurement.reference_value === 1
407 ? variant.unit_price_measurement.reference_unit
408 : variant.unit_price_measurement.reference_value +
409 variant.unit_price_measurement.reference_unit;
410 }
411
412 return {
413 formatMoney: formatMoney,
414 getBaseUnit: getBaseUnit
415 }
416 })();
417
418
419 /**
420 * Image Helper Functions
421 * -----------------------------------------------------------------------------
422 * A collection of functions that help with basic image operations.
423 *
424 */
425
426 theme.Images = (function() {
427
428 /**
429 * Find the Shopify image attribute size
430 *
431 * @param {string} src
432 * @returns {null}
433 */
434 function imageSize(src) {
435 if (!src) {
436 return '620x'; // default based on theme
437 }
438
439 var match = src.match(/.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\.@]/);
440
441 if (match !== null) {
442 return match[1];
443 } else {
444 return null;
445 }
446 }
447
448 /**
449 * Adds a Shopify size attribute to a URL
450 *
451 * @param src
452 * @param size
453 * @returns {*}
454 */
455 function getSizedImageUrl(src, size) {
456 if (size == null) {
457 return src;
458 }
459
460 if (size === 'master') {
461 return this.removeProtocol(src);
462 }
463
464 var match = src.match(/\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i);
465
466 if (match != null) {
467 var prefix = src.split(match[0]);
468 var suffix = match[0];
469
470 return this.removeProtocol(prefix[0] + '_' + size + suffix);
471 }
472
473 return null;
474 }
475
476 function removeProtocol(path) {
477 return path.replace(/http(s)?:/, '');
478 }
479
480 return {
481 imageSize: imageSize,
482 getSizedImageUrl: getSizedImageUrl,
483 removeProtocol: removeProtocol
484 };
485 })();
486
487 slate.Variants = (function() {
488
489 function Variants(options) {
490 this.$container = options.$container;
491 this.variants = options.variants;
492 this.singleOptionSelector = options.singleOptionSelector;
493 this.originalSelectorId = options.originalSelectorId;
494 this.enableHistoryState = options.enableHistoryState;
495 this.currentVariant = this._getVariantFromOptions();
496
497 $(this.singleOptionSelector, this.$container).on('change', this._onSelectChange.bind(this));
498 }
499
500 Variants.prototype = $.extend({}, Variants.prototype, {
501
502 _getCurrentOptions: function() {
503 var currentOptions = $.map($(this.singleOptionSelector, this.$container), function(element) {
504 var $element = $(element);
505 var type = $element.attr('type');
506 var currentOption = {};
507
508 if (type === 'radio' || type === 'checkbox') {
509 if ($element[0].checked) {
510 currentOption.value = $element.val();
511 currentOption.index = $element.data('index');
512
513 return currentOption;
514 } else {
515 return false;
516 }
517 } else {
518 currentOption.value = $element.val();
519 currentOption.index = $element.data('index');
520
521 return currentOption;
522 }
523 });
524
525 // remove any unchecked input values if using radio buttons or checkboxes
526 currentOptions = this._compact(currentOptions);
527
528 return currentOptions;
529 },
530
531 _getVariantFromOptions: function() {
532 var selectedValues = this._getCurrentOptions();
533 var variants = this.variants;
534 var found = false;
535
536 variants.forEach(function(variant) {
537 var match = true;
538 var options = variant.options;
539
540 selectedValues.forEach(function(option) {
541 if (match) {
542 match = (variant[option.index] === option.value);
543 }
544 });
545
546 if (match) {
547 found = variant;
548 }
549 });
550
551 return found || null;
552 },
553
554 _onSelectChange: function() {
555 var variant = this._getVariantFromOptions();
556
557 this.$container.trigger({
558 type: 'variantChange',
559 variant: variant
560 });
561
562 document.dispatchEvent(new CustomEvent('variant:change', {
563 detail: {
564 variant: variant
565 }
566 }));
567
568 if (!variant) {
569 return;
570 }
571
572 this._updateMasterSelect(variant);
573 this._updateImages(variant);
574 this._updatePrice(variant);
575 this._updateUnitPrice(variant);
576 this._updateSKU(variant);
577 this.currentVariant = variant;
578
579 if (this.enableHistoryState) {
580 this._updateHistoryState(variant);
581 }
582 },
583
584 _updateImages: function(variant) {
585 var variantImage = variant.featured_image || {};
586 var currentVariantImage = this.currentVariant.featured_image || {};
587
588 if (!variant.featured_image || variantImage.src === currentVariantImage.src) {
589 return;
590 }
591
592 this.$container.trigger({
593 type: 'variantImageChange',
594 variant: variant
595 });
596 },
597
598 _updatePrice: function(variant) {
599 if (variant.price === this.currentVariant.price && variant.compare_at_price === this.currentVariant.compare_at_price) {
600 return;
601 }
602
603 this.$container.trigger({
604 type: 'variantPriceChange',
605 variant: variant
606 });
607 },
608
609 _updateUnitPrice: function(variant) {
610 if (variant.unit_price === this.currentVariant.unit_price) {
611 return;
612 }
613
614 this.$container.trigger({
615 type: 'variantUnitPriceChange',
616 variant: variant
617 });
618 },
619
620 _updateSKU: function(variant) {
621 if (variant.sku === this.currentVariant.sku) {
622 return;
623 }
624
625 this.$container.trigger({
626 type: 'variantSKUChange',
627 variant: variant
628 });
629 },
630
631 _updateHistoryState: function(variant) {
632 if (!history.replaceState || !variant) {
633 return;
634 }
635
636 var newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?variant=' + variant.id;
637 window.history.replaceState({path: newurl}, '', newurl);
638 },
639
640 _updateMasterSelect: function(variant) {
641 $(this.originalSelectorId, this.$container).val(variant.id);
642 },
643
644 // _.compact from lodash
645 // https://github.com/lodash/lodash/blob/4d4e452ade1e78c7eb890968d851f837be37e429/compact.js
646 _compact: function(array) {
647 var index = -1,
648 length = array == null ? 0 : array.length,
649 resIndex = 0,
650 result = [];
651
652 while (++index < length) {
653 var value = array[index];
654 if (value) {
655 result[resIndex++] = value;
656 }
657 }
658 return result;
659 }
660 });
661
662 return Variants;
663 })();
664
665 slate.rte = {
666 init: function() {
667 slate.rte.wrapTable();
668 slate.rte.wrapVideo();
669 slate.rte.imageLinks();
670 },
671
672 wrapTable: function() {
673 $('.rte table').wrap('<div class="table-wrapper"></div>');
674 },
675
676 wrapVideo: function() {
677 var $iframeVideo = $('.rte iframe[src*="youtube.com/embed"], .rte iframe[src*="player.vimeo"]');
678 var $iframeReset = $iframeVideo.add('iframe#admin_bar_iframe');
679
680 $iframeVideo.each(function () {
681 // Add wrapper to make video responsive
682 if (!$(this).parents('.video-wrapper').length) {
683 $(this).wrap('<div class="video-wrapper"></div>');
684 }
685 });
686
687 $iframeReset.each(function () {
688 // Re-set the src attribute on each iframe after page load
689 // for Chrome's "incorrect iFrame content on 'back'" bug.
690 // https://code.google.com/p/chromium/issues/detail?id=395791
691 // Need to specifically target video and admin bar
692 this.src = this.src;
693 });
694 },
695
696 // Remove CSS that adds animated underline under image links
697 imageLinks: function() {
698 $('.rte a img').parent().addClass('rte__image');
699 }
700 };
701
702
703 theme.Modals = (function() {
704 function Modal(id, name, options) {
705 var defaults = {
706 close: '.js-modal-close',
707 open: '.js-modal-open-' + name,
708 openClass: 'modal--is-active',
709 bodyOpenClass: 'modal-open',
710 closeOffContentClick: true
711 };
712
713 this.id = id;
714 this.$modal = $('#' + id);
715
716 if (!this.$modal.length) {
717 return false;
718 }
719
720 this.nodes = {
721 $parent: $('html').add('body'),
722 $modalContent: this.$modal.find('.modal__inner')
723 };
724
725 this.config = $.extend(defaults, options);
726 this.modalIsOpen = false;
727 this.$focusOnOpen = this.config.focusOnOpen ? $(this.config.focusOnOpen) : this.$modal;
728
729 this.init();
730 }
731
732 Modal.prototype.init = function() {
733 var $openBtn = $(this.config.open);
734
735 // Add aria controls
736 $openBtn.attr('aria-expanded', 'false');
737
738 $(this.config.open).on('click', this.open.bind(this));
739 this.$modal.find(this.config.close).on('click', this.close.bind(this));
740
741 // Close modal if a drawer is opened
742 $('body').on('drawerOpen', function() {
743 this.close();
744 }.bind(this));
745 };
746
747 Modal.prototype.open = function(evt) {
748 // Keep track if modal was opened from a click, or called by another function
749 var externalCall = false;
750
751 // don't open an opened modal
752 if (this.modalIsOpen) {
753 return;
754 }
755
756 // Prevent following href if link is clicked
757 if (evt) {
758 evt.preventDefault();
759 } else {
760 externalCall = true;
761 }
762
763 // Without this, the modal opens, the click event bubbles up to $nodes.page
764 // which closes the modal.
765 if (evt && evt.stopPropagation) {
766 evt.stopPropagation();
767 // save the source of the click, we'll focus to this on close
768 this.$activeSource = $(evt.currentTarget);
769 }
770
771 if (this.modalIsOpen && !externalCall) {
772 this.close();
773 }
774
775 this.$modal.addClass(this.config.openClass);
776 this.nodes.$parent.addClass(this.config.bodyOpenClass);
777
778 setTimeout(function() {
779 this.$modal.addClass('aos-animate');
780 }.bind(this), 0);
781
782 this.modalIsOpen = true;
783
784 slate.a11y.trapFocus({
785 $container: this.$modal,
786 $elementToFocus: this.$focusOnOpen,
787 namespace: 'modal_focus'
788 });
789
790 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
791 this.$activeSource.attr('aria-expanded', 'true');
792 }
793
794 $('body').trigger('modalOpen.' + this.id);
795
796 this.bindEvents();
797 };
798
799 Modal.prototype.close = function() {
800 // don't close a closed modal
801 if (!this.modalIsOpen) {
802 return;
803 }
804
805 // deselect any focused form elements
806 $(document.activeElement).trigger('blur');
807
808 this.$modal.removeClass(this.config.openClass).removeClass('aos-animate');
809 this.nodes.$parent.removeClass(this.config.bodyOpenClass);
810
811 this.modalIsOpen = false;
812
813 slate.a11y.removeTrapFocus({
814 $container: this.$modal,
815 namespace: 'modal_focus'
816 });
817
818 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
819 this.$activeSource.attr('aria-expanded', 'false').focus();
820 }
821
822 $('body').trigger('modalClose.' + this.id);
823
824 this.unbindEvents();
825 };
826
827 Modal.prototype.bindEvents = function() {
828 // Pressing escape closes modal
829 this.nodes.$parent.on('keyup.modal', function(evt) {
830 if (evt.keyCode === 27) {
831 this.close();
832 }
833 }.bind(this));
834
835 if (this.config.closeOffContentClick) {
836 // Clicking outside of the modal content also closes it
837 this.$modal.on('click.modal', this.close.bind(this));
838
839 // Exception to above: clicking anywhere on the modal content will NOT close it
840 this.nodes.$modalContent.on('click.modal', function(evt) {
841 evt.stopImmediatePropagation();
842 });
843 }
844 };
845
846 Modal.prototype.unbindEvents = function() {
847 this.nodes.$parent.off('.modal');
848
849 if (this.config.closeOffContentClick) {
850 this.$modal.off('.modal');
851 this.nodes.$modalContent.off('.modal');
852 }
853 };
854
855 return Modal;
856 })();
857
858 theme.ProductScreen = (function() {
859
860 var originalTitle = document.title;
861 var namespace = 'productscreen';
862 var windowPosition = 0;
863 var $page = $('#MainContent');
864
865 function ProductScreen(id, name, options) {
866 var defaults = {
867 close: '.js-screen-close',
868 open: '.js-screen-open-' + name,
869 openClass: 'screen-layer--is-active',
870 closeSlideAnimate: 'screen-layer--is-sliding',
871 bodyOpenClass: 'screen-layer-open',
872 bodyClosingClass: 'screen-layer-closing',
873 bodyCloseAnimate: 'screen-layer-closing screen-layer-animating',
874 loaderStart: 200,
875 pullToCloseThreshold: -100
876 };
877
878 this.id = id;
879 this.$screen = $('#' + id);
880 this.title = this.$screen.data('product-title');
881
882 if (!this.$screen.length) {
883 return false;
884 }
885
886 this.nodes = {
887 $parent: $('html').add('body'),
888 $body: $('body'),
889 $loader: $('#OverscrollLoader').find('.icon-loader__path'),
890 $screenContent: this.$screen.find('.screen-layer__inner'),
891 $photoswipe: $('.pswp')
892 };
893
894 this.config = $.extend(defaults, options);
895 this.initalized = false; // opened at least once
896 this.isOpen = false;
897 this.$focusOnOpen = this.config.focusOnOpen ? $(this.config.focusOnOpen) : this.$screen;
898
899 this.init();
900 }
901
902 ProductScreen.prototype.init = function() {
903 var $openBtn = $(this.config.open);
904
905 // Add aria controls
906 $openBtn.attr('aria-expanded', 'false');
907
908 $(this.config.open).on('click', this.open.bind(this));
909 this.$screen.find(this.config.close).on('click', { noAnimate: true, back: true }, this.close.bind(this));
910
911 // Close screen if product added to sticky cart
912 if (theme.settings.cartType === 'sticky') {
913 this.nodes.$body.on('added.' + this.id, function() {
914 theme.headerNav.toggleThumbMenu(false, true);
915 var args = { back: true };
916 this.close(false, args);
917 }.bind(this));
918
919 this.nodes.$body.on('error.' + this.id, function() {
920 if (this.initalized) {
921 this.open();
922 }
923 }.bind(this));
924 }
925 };
926
927 ProductScreen.prototype.open = function(evt, data) {
928 // Keep track if modal was opened from a click, or called by another function
929 var externalCall = false;
930 var args = {
931 updateCurrentPath: data ? data.updateCurrentPath : true
932 };
933
934 if (this.isOpen) {
935 return;
936 }
937
938 // Prevent following href if link is clicked
939 if (evt) {
940 evt.preventDefault();
941 } else {
942 externalCall = true;
943 }
944
945 // Without this, the modal opens, the click event bubbles up to $nodes.page
946 // which closes the modal.
947 if (evt && evt.stopPropagation) {
948 evt.stopPropagation();
949 // save the source of the click, we'll focus to this on close
950 this.$activeSource = $(evt.currentTarget);
951 }
952
953 if (this.isOpen && !externalCall) {
954 this.close();
955 }
956
957 windowPosition = window.scrollY;
958
959 this.$screen
960 .prepareTransition()
961 .addClass(this.config.openClass);
962 this.nodes.$parent.addClass(this.config.bodyOpenClass);
963 this.nodes.$screenContent.scrollTop(0);
964 window.scrollTo(0,0);
965
966 slate.a11y.trapFocus({
967 $container: this.$screen,
968 $elementToFocus: this.$focusOnOpen,
969 namespace: namespace
970 });
971
972 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
973 this.$activeSource.attr('aria-expanded', 'true');
974 }
975
976 var newUrl = this.$activeSource.data('url');
977 this.nodes.$body
978 .trigger('productModalOpen.' + this.id)
979 .trigger('newPopstate', {screen: this, url: newUrl, updateCurrentPath: args.updateCurrentPath});
980
981 this.initalized = true;
982 this.isOpen = true;
983 document.title = this.title;
984
985 // Trigger Google Analytics page view if enabled
986 if (window.ga) { ga('send', 'pageview', { page: newUrl }) }
987
988 this.bindEvents();
989 };
990
991 ProductScreen.prototype.close = function(evt, args) {
992 var evtData = args ? args : (evt ? evt.data : null);
993 var goBack = evtData ? evtData.back : false;
994 var noAnimate = (evtData && evtData.noAnimate) ? true : false;
995 this.nodes.$body.removeAttr('style');
996 this.nodes.$loader.css('stroke-dashoffset', this.config.loaderStart);
997
998 if (goBack) {
999 this.nodes.$body.trigger('newPopstate', {screen: this, back: true});
1000 }
1001
1002 var closeClass = noAnimate ? '' : this.config.closeSlideAnimate;
1003 var bodyCloseClass = noAnimate ? this.config.bodyClosingClass : this.config.bodyCloseAnimate;
1004
1005 // Don't close if already closed
1006 if (!this.isOpen) {
1007 return;
1008 }
1009
1010 // deselect any focused form elements
1011 $(document.activeElement).trigger('blur');
1012
1013 this.$screen
1014 .prepareTransition()
1015 .removeClass(this.config.openClass)
1016 .addClass(closeClass);
1017 this.nodes.$parent
1018 .removeClass(this.config.bodyOpenClass)
1019 .addClass(bodyCloseClass);
1020
1021 window.setTimeout(function() {
1022 this.$screen.removeClass(closeClass);
1023 this.nodes.$parent.removeClass(bodyCloseClass);
1024 window.scrollTo(0, windowPosition);
1025 }.bind(this), 500); // duration of css animation
1026
1027 slate.a11y.removeTrapFocus({
1028 $container: this.$screen,
1029 namespace: namespace
1030 });
1031
1032 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
1033 this.$activeSource.attr('aria-expanded', 'false').focus();
1034 }
1035
1036 this.nodes.$body
1037 .trigger('productModalClose')
1038 .trigger('productModalClose.' + this.id);
1039
1040 window.scrollTo(0, windowPosition);
1041
1042 this.isOpen = false;
1043 document.title = originalTitle;
1044
1045 if (window.ga) { ga('send', 'pageview') }
1046
1047 this.unbindEvents();
1048 };
1049
1050 ProductScreen.prototype.bindEvents = function() {
1051 // Pressing escape closes modal, unless the photoswipe screen is open
1052 this.nodes.$body.on('keyup.' + namespace, function(evt) {
1053 if (evt.keyCode === 27) {
1054 if (this.nodes.$photoswipe.hasClass('pswp--open')) {
1055 return;
1056 }
1057 if (this.nodes.$body.hasClass('js-drawer-open')) {
1058 return;
1059 }
1060 var args = { back: true };
1061 this.close(false, args);
1062 }
1063 }.bind(this));
1064
1065 // If scrolling up while at top, close modal
1066 var bgAmount = 0;
1067 var loaderAmount = 0;
1068 $(document).on('touchmove.' + namespace, $.throttle(15, function(evt) {
1069 var pos = window.scrollY;
1070
1071 if (pos >= 0) {
1072 return;
1073 }
1074
1075 bgAmount = -(pos/100);
1076 this.nodes.$body.css('background', 'rgba(0,0,0,' + bgAmount + ')');
1077
1078 // stroke fills from 200-0 (0 = full)
1079 loaderAmount = this.config.loaderStart + (pos * 2); // pos is negative number
1080
1081 if (pos <= this.config.pullToCloseThreshold) {
1082 loaderAmount = 0;
1083 }
1084
1085 this.nodes.$loader.css('stroke-dashoffset', loaderAmount);
1086 }.bind(this)));
1087
1088 $(document).on('touchend.' + namespace, function(evt) {
1089 totalLoader = this.config.loaderStart; // reset to starting point
1090 var pos = window.scrollY;
1091 if (pos < this.config.pullToCloseThreshold) {
1092 var args = { back: true };
1093 this.close(false, args);
1094 }
1095 }.bind(this));
1096 };
1097
1098 ProductScreen.prototype.unbindEvents = function() {
1099 this.nodes.$body.off('.' + namespace);
1100 $(document).off('.' + namespace);
1101 };
1102
1103 return ProductScreen;
1104 })();
1105
1106 theme.Drawers = (function() {
1107 function Drawer(id, name) {
1108 this.config = {
1109 id: id,
1110 close: '.js-drawer-close',
1111 open: '.js-drawer-open-' + name,
1112 openClass: 'js-drawer-open',
1113 closingClass: 'js-drawer-closing',
1114 activeDrawer: 'drawer--is-open',
1115 namespace: '.drawer-' + name
1116 };
1117
1118 this.$nodes = {
1119 parent: $(document.documentElement).add('body'),
1120 page: $('body')
1121 };
1122
1123 this.$drawer = $('#' + id);
1124
1125 if (!this.$drawer.length) {
1126 return false;
1127 }
1128
1129 this.isOpen = false;
1130 this.init();
1131 };
1132
1133 Drawer.prototype = $.extend({}, Drawer.prototype, {
1134 init: function() {
1135 var $openBtn = $(this.config.open);
1136
1137 // Add aria controls
1138 $openBtn.attr('aria-expanded', 'false');
1139
1140 $openBtn.on('click', this.open.bind(this));
1141 this.$drawer.find(this.config.close).on('click', this.close.bind(this));
1142 },
1143
1144 open: function(evt) {
1145 if (evt) {
1146 evt.preventDefault();
1147 }
1148
1149 if (this.isOpen) {
1150 return;
1151 }
1152
1153 // Without this the drawer opens, the click event bubbles up to $nodes.page which closes the drawer.
1154 if (evt && evt.stopPropagation) {
1155 evt.stopPropagation();
1156 // save the source of the click, we'll focus to this on close
1157 this.$activeSource = $(evt.currentTarget);
1158 }
1159
1160 this.$drawer.prepareTransition().addClass(this.config.activeDrawer);
1161
1162 this.$nodes.parent.addClass(this.config.openClass);
1163 this.isOpen = true;
1164
1165 slate.a11y.trapFocus({
1166 $container: this.$drawer,
1167 namespace: 'drawer_focus'
1168 });
1169
1170 $('body').trigger('drawerOpen.' + this.config.id);
1171
1172 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
1173 this.$activeSource.attr('aria-expanded', 'true');
1174 }
1175
1176 this.bindEvents();
1177 },
1178
1179 close: function() {
1180 if (!this.isOpen) {
1181 return;
1182 }
1183
1184 // deselect any focused form elements
1185 $(document.activeElement).trigger('blur');
1186
1187 this.$drawer.prepareTransition().removeClass(this.config.activeDrawer);
1188
1189 this.$nodes.parent.removeClass(this.config.openClass);
1190 this.$nodes.parent.addClass(this.config.closingClass);
1191 var o = this;
1192 window.setTimeout(function() {
1193 o.$nodes.parent.removeClass(o.config.closingClass);
1194 }, 500);
1195
1196 this.isOpen = false;
1197
1198 slate.a11y.removeTrapFocus({
1199 $container: this.$drawer,
1200 namespace: 'drawer_focus'
1201 });
1202
1203 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
1204 this.$activeSource.attr('aria-expanded', 'false');
1205 }
1206
1207 this.unbindEvents();
1208 },
1209
1210 bindEvents: function() {
1211 slate.a11y.lockMobileScrolling(this.config.namespace, this.$nodes.page);
1212
1213 // Clicking body closes drawer
1214 this.$nodes.page.on('click' + this.config.namespace, function (evt) {
1215 if (evt.target === this.$nodes.page[0]) {
1216 this.close();
1217 return false;
1218 }
1219 }.bind(this));
1220
1221 // Pressing escape closes drawer
1222 this.$nodes.parent.on('keyup' + this.config.namespace, function(evt) {
1223 if (evt.keyCode === 27) {
1224 this.close();
1225 }
1226 }.bind(this));
1227 },
1228
1229 unbindEvents: function() {
1230 slate.a11y.unlockMobileScrolling(this.config.namespace, this.$nodes.page);
1231 this.$nodes.parent.off(this.config.namespace);
1232 this.$nodes.page.off(this.config.namespace);
1233 }
1234 });
1235
1236 return Drawer;
1237 })();
1238
1239 theme.cart = {
1240 getCart: function() {
1241 return $.getJSON('/cart.js');
1242 },
1243
1244 changeItem: function(key, qty) {
1245 return this._updateCart({
1246 type: 'POST',
1247 url: '/cart/change.js',
1248 data: 'quantity=' + qty + '&id=' + key,
1249 dataType: 'json'
1250 });
1251 },
1252
1253 addItemFromForm: function(data) {
1254 return this._updateCart({
1255 type: 'POST',
1256 url: '/cart/add.js',
1257 data: data,
1258 dataType: 'json'
1259 });
1260 },
1261
1262 _updateCart: function(params) {
1263 return $.ajax(params)
1264 .then(function(cart) {
1265 $('body').trigger('updateCart', cart);
1266 return cart;
1267 }.bind(this))
1268 },
1269
1270 updateNote: function(note) {
1271 var params = {
1272 type: 'POST',
1273 url: '/cart/update.js',
1274 data: 'note=' + theme.cart.attributeToString(note),
1275 dataType: 'json',
1276 success: function(cart) {},
1277 error: function(XMLHttpRequest, textStatus) {}
1278 };
1279
1280 $.ajax(params);
1281 },
1282
1283 updateCurrency: function(code) {
1284 var params = {
1285 type: 'POST',
1286 url: '/cart/update.js',
1287 data: 'currency=' + code,
1288 dataType: 'json',
1289 success: function(cart) {
1290 location.reload(); // required for multi-currency feature to update
1291 },
1292 error: function(XMLHttpRequest, textStatus) {}
1293 };
1294
1295 $.ajax(params);
1296 },
1297
1298 attributeToString: function(attribute) {
1299 if ((typeof attribute) !== 'string') {
1300 attribute += '';
1301 if (attribute === 'undefined') {
1302 attribute = '';
1303 }
1304 }
1305 return $.trim(attribute);
1306 }
1307 }
1308
1309 $(function() {
1310 // Add a loading indicator on the cart checkout button (/cart and drawer)
1311 $('body').on('click', '.cart__checkout', function() {
1312 $(this).addClass('btn--loading');
1313 });
1314
1315 $('body').on('change', 'textarea[name="note"]', function() {
1316 var newNote = $(this).val();
1317 theme.cart.updateNote(newNote);
1318 });
1319
1320
1321 // Custom JS to prevent checkout without confirming terms and conditions
1322 $('body').on('click', '.cart__checkout--ajax', function(evt) {
1323 if ($('#CartAgree').is(':checked')) {
1324 } else {
1325 alert(theme.strings.cartTermsConfirmation);
1326 $(this).removeClass('btn--loading');
1327 return false;
1328 }
1329 });
1330
1331 $('body').on('click', '.cart__checkout--page', function(evt) {
1332 if ($('#CartPageAgree').is(':checked')) {
1333 } else {
1334 alert(theme.strings.cartTermsConfirmation);
1335 $(this).removeClass('btn--loading');
1336 return false;
1337 }
1338 });
1339 });
1340
1341 theme.AjaxCart = (function() {
1342 var config = {
1343 namespace: '.ajaxcart'
1344 };
1345
1346 var data = {
1347 itemId: 'data-cart-item-id'
1348 };
1349
1350 var selectors = {
1351 form: 'form.cart',
1352 cartCount: '.cart-link__count',
1353 updateBtn: '.update-cart',
1354
1355 itemList: '[data-cart-item-list]',
1356 item: '[data-cart-item]',
1357 itemId: '[data-cart-item-id]',
1358 itemHref: '[data-cart-item-href]',
1359 itemBackgroundImage: '[data-cart-item-background-image]',
1360 itemTitle: '[data-cart-item-title]',
1361 itemVariantTitle: '[data-cart-item-variant-title]',
1362 itemPropertyList: '[data-cart-item-property-list]',
1363 itemProperty: '[data-cart-item-property]',
1364 itemDiscountList: '[data-cart-item-discount-list]',
1365 itemDiscount: '[data-cart-item-discount]',
1366 itemDiscountTitle: '[data-cart-item-discount-title]',
1367 itemDiscountAmount: '[data-cart-item-discount-amount]',
1368 itemLabelQuantity: '[data-cart-item-label-quantity]',
1369 itemInputQuantity: '[data-cart-item-input-quantity]',
1370 itemDelete: '[data-cart-item-delete]',
1371 itemPriceContainer: '[data-cart-item-price-container]',
1372 itemLinePriceContainer: '[data-cart-item-line-price-container]',
1373 itemUnitPrice: '[data-cart-item-unit-price]',
1374 itemMessage: '[data-item-message]',
1375 cartDiscountContainer: '[data-cart-discount-container]',
1376 cartDiscountContent: '[data-cart-discount-content]',
1377 cartDiscount: '[data-cart-discount]',
1378 cartDiscountTitle: '[data-cart-discount-title]',
1379 cartDiscountAmount: '[data-cart-discount-amount]',
1380 cartNoteContainer: '[data-cart-note-container]',
1381 cartNoteInput: '[data-cart-note]',
1382 cartMessage: '[data-cart-message]',
1383 cartSubtotal: '[data-cart-subtotal]',
1384 cartSubmit: '[data-cart-submit]',
1385
1386 fixedFooter: '.drawer__footer--fixed',
1387 fixedInnerContent: '.drawer__inner--has-fixed-footer'
1388 };
1389
1390 var classes = {
1391 cartHasItems: 'cart-has-items',
1392 cartTemplate: 'ajax-cart__template',
1393 cartItemRemove: 'cart__item--remove',
1394 staticDrawerElement: 'drawer--cart--static'
1395 };
1396
1397 function AjaxCart(id) {
1398 this.id = id;
1399 var $container = this.$container = $('#' + id);
1400
1401 this.status = {
1402 loaded: false,
1403 loading: false,
1404 isDrawer: $container.attr('data-drawer')
1405 };
1406
1407 if (this.status.isDrawer) {
1408 this.drawer = new theme.Drawers(id, 'cart');
1409 }
1410
1411 this.init();
1412 this.initEventListeners();
1413 };
1414
1415 AjaxCart.prototype = $.extend({}, AjaxCart.prototype, {
1416 init: function() {
1417 this.$form = $(selectors.form, this.$container);
1418 $(selectors.updateBtn, this.$form).addClass('hide');
1419 this.$itemTemplate = $(selectors.item, this.$form).first().clone();
1420 this.$propertyTemplate = $(selectors.itemProperty, this.$form).first().clone();
1421 this.$discountTemplate = $(selectors.itemDiscount, this.$form).first().clone();
1422 this.$cartDiscountTemplate = $(selectors.cartDiscount, this.$container).first().clone();
1423 },
1424
1425 initEventListeners: function() {
1426 $('body').on('updateCart', function(evt, cart) {
1427 theme.cart.getCart().then(function(cart) {
1428 this.buildCart(cart);
1429 this.updateCartNotification(cart);
1430
1431 // Open cart once updated
1432 var openDrawer = false;
1433 if (this.status.isDrawer) {
1434 this.drawer.open();
1435 openDrawer = true;
1436 }
1437 }.bind(this));
1438 }.bind(this));
1439
1440 this.$container.on('click', selectors.itemDelete, this._onItemDelete.bind(this));
1441 this.$container.on('input', selectors.itemInputQuantity, $.debounce(500, this._onItemQuantityChange.bind(this)));
1442 this.$container.on('blur', selectors.itemInputQuantity, this._onItemQuantityEmptyBlur.bind(this));
1443 this.$container.on('focus', selectors.itemInputQuantity, this._highlightText);
1444
1445 if (this.status.isDrawer) {
1446 $('body')
1447 .on('updateCart' + config.namespace, this.sizeCart.bind(this))
1448 .on('drawerOpen.' + this.id, function() {
1449 this.sizeCart();
1450 setTimeout(function() {
1451 this.sizeCart();
1452 }.bind(this), 500);
1453 }.bind(this));
1454
1455 $(window).on('resize' + config.namespace, $.debounce(150, this.sizeCart.bind(this)));
1456 }
1457 },
1458
1459 buildCart: function(cart, openDrawer) {
1460 this.loading(true);
1461
1462 this.$form.removeClass('cart--empty');
1463
1464 if (cart.item_count === 0) {
1465 this.$form.addClass('cart--empty');
1466 this.status.loaded = true;
1467 this.loading(false);
1468 return;
1469 }
1470
1471 // If 3+ items, remove static class for a 100% height cart drawer
1472 if (cart.items.length > 2) {
1473 this.$container.removeClass(classes.staticDrawerElement);
1474 } else {
1475 this.$container.addClass(classes.staticDrawerElement);
1476 }
1477
1478 var $cart = this._createCart(cart);
1479 morphdom(this.$form[0], $cart[0]);
1480
1481 if (theme.settings.currenciesEnabled) {
1482 theme.currencySwitcher.ajaxrefresh();
1483 }
1484
1485 if (window.Shopify && Shopify.StorefrontExpressButtons) {
1486 Shopify.StorefrontExpressButtons.initialize();
1487
1488 // Resize footer after arbitrary delay to account for checkout buttons
1489 if (this.status.isDrawer) {
1490 setTimeout(function() {
1491 this.sizeCart();
1492 }.bind(this), 1000);
1493 }
1494 }
1495
1496 // If specifically asked, open the cart drawer (only happens after product added from form)
1497 if (this.status.isDrawer) {
1498 if (openDrawer === true) {
1499 this.drawer.open();
1500 }
1501 }
1502
1503 this.status.loaded = true;
1504 this.loading(false);
1505
1506 document.dispatchEvent(new CustomEvent('cart:updated', {
1507 detail: {
1508 cart: cart
1509 }
1510 }));
1511 },
1512
1513 _createCart: function(cart) {
1514 var $form = this.$form.clone();
1515
1516 $(selectors.item, $form)
1517 .not(selectors.cartNoteContainer)
1518 .remove();
1519
1520 $(selectors.itemList, $form)
1521 .prepend(this._createItemList(cart));
1522
1523 $(selectors.cartNoteInput, $form)
1524 .val(cart.note);
1525
1526 $(selectors.cartDiscountContainer, $form)
1527 .toggleClass('hide', cart.cart_level_discount_applications.length === 0);
1528
1529 $(selectors.cartDiscountContent, $form).html(
1530 this._createCartDiscountList(cart));
1531
1532 $(selectors.cartSubtotal, $form)
1533 .html(theme.Currency.formatMoney(cart.total_price, theme.settings.moneyFormat));
1534
1535 $(selectors.cartSubmit, $form).attr('disabled', cart.items.length === 0);
1536
1537 return $form;
1538 },
1539
1540 _createItemList: function(cart) {
1541 return $.map(cart.items, function(item) {
1542 var $item = this.$itemTemplate.clone().removeClass(classes.cartTemplate);
1543 var propertyList = this._createPropertyList(item);
1544 var discountList = this._createDiscountList(item);
1545 var unitPrice = this._createUnitPrice(item);
1546
1547 var itemPrice = this._createItemPrice(
1548 item.original_price,
1549 item.final_price
1550 );
1551
1552 var itemLinePrice = this._createItemPrice(
1553 item.original_line_price,
1554 item.final_line_price
1555 );
1556
1557 $item.find(selectors.itemId)
1558 .addBack(selectors.itemId)
1559 .attr(data.itemId, item.key);
1560
1561 $(selectors.itemHref, $item)
1562 .attr('href', item.url);
1563
1564 $(selectors.itemBackgroundImage, $item)
1565 .removeAttr('data-bgset')
1566 .css('background-image', item.image ? 'url(' + theme.Images.getSizedImageUrl(item.image, '200x') + ')' : 'none')
1567 .addClass('lazyload');
1568
1569 $(selectors.itemTitle, $item).text(item.product_title);
1570
1571 $(selectors.itemVariantTitle, $item).text(item.variant_title);
1572
1573 $(selectors.itemPriceContainer, $item).html(itemPrice);
1574
1575 $(selectors.itemLinePriceContainer, $item).html(itemLinePrice);
1576
1577 $(selectors.itemLinePrice, $item)
1578 .html(theme.Currency.formatMoney(item.line_price, theme.settings.moneyFormat));
1579
1580 $(selectors.itemLabelQuantity, $item)
1581 .attr('for', 'quantity_' + item.key);
1582
1583 $(selectors.itemInputQuantity, $item)
1584 .attr('name', 'updates[' + item.key + ']')
1585 .attr('id', 'quantity_' + item.key)
1586 .val(item.quantity);
1587
1588 $(selectors.itemPropertyList, $item)
1589 .html(propertyList);
1590
1591 $(selectors.itemDiscountList, $item)
1592 .html(discountList);
1593
1594 $(selectors.itemUnitPrice, $item)
1595 .html(unitPrice);
1596
1597 return $item[0];
1598 }.bind(this));
1599 },
1600
1601 _createItemPrice: function(original_price, final_price) {
1602 if (original_price !== final_price) {
1603 return (
1604 '<span class="visually-hidden">' + theme.strings.regularPrice + '</span>' +
1605 '<del class="cart__item-price--original">' +
1606 theme.Currency.formatMoney(original_price, theme.settings.moneyFormat) +
1607 '</del>' +
1608 '<span class="visually-hidden">' + theme.strings.salePrice + '</span>' +
1609 '<span class="cart__item-price cart__item-price--bold">' +
1610 theme.Currency.formatMoney(final_price, theme.settings.moneyFormat) +
1611 '</span>'
1612 );
1613 } else {
1614 return (
1615 '<span class="cart__item-price">' + theme.Currency.formatMoney(original_price, theme.settings.moneyFormat) + '</span>'
1616 );
1617 }
1618 },
1619
1620 _createPropertyList: function(item) {
1621 return $.map(item.properties, function(value, key) {
1622 var $property = this.$propertyTemplate.clone().removeClass(classes.cartTemplate);
1623
1624 // Line item properties prefixed with an underscore are not to be displayed
1625 if (key.charAt(0) === '_') return;
1626
1627 // Line item properties with no value are not to be displayed
1628 if (value === '') return;
1629
1630 if (value.indexOf('/uploads/') === -1) {
1631 $property
1632 .text(key + ': ' + value);
1633 } else {
1634 $property
1635 .html(key + ': <a href="' + value + '">' + value.split('/').pop() + '</a>');
1636 }
1637
1638 return $property[0];
1639 }.bind(this));
1640 },
1641
1642 _createDiscountList: function(item) {
1643 return $.map(item.line_level_discount_allocations, function(discount) {
1644 var $discount = this.$discountTemplate.clone().removeClass(classes.cartTemplate);
1645
1646 $discount
1647 .find(selectors.itemDiscountTitle)
1648 .text(discount.discount_application.title);
1649 $discount
1650 .find(selectors.itemDiscountAmount)
1651 .html(theme.Currency.formatMoney(discount.amount, theme.settings.moneyFormat));
1652
1653 return $discount[0];
1654 }.bind(this));
1655 },
1656
1657 _createCartDiscountList: function(cart) {
1658 return $.map(
1659 cart.cart_level_discount_applications,
1660 function(discount) {
1661 var $discount = this.$cartDiscountTemplate.clone().removeClass(classes.cartTemplate);
1662
1663 $discount.find(selectors.cartDiscountTitle)
1664 .text(discount.title);
1665 $discount
1666 .find(selectors.cartDiscountAmount)
1667 .html(theme.Currency.formatMoney(discount.total_allocated_amount, theme.settings.moneyFormat));
1668
1669 return $discount[0];
1670 }.bind(this)
1671 );
1672 },
1673
1674 _createUnitPrice: function(item) {
1675 var price = theme.Currency.formatMoney(item.unit_price, theme.settings.moneyFormat);
1676 var base = theme.Currency.getBaseUnit(item);
1677
1678 return price + '/' + base;
1679 },
1680
1681 _onItemQuantityChange: function(evt) {
1682 this.loading(true);
1683
1684 var $input = $(evt.target);
1685 var id = $input.closest(selectors.item).attr(data.itemId);
1686 var quantity = $input.val();
1687
1688 // Don't update the cart when a input is empty. Also make sure an input
1689 // does not remain empty by checking blur event
1690 if (quantity === '') { return; }
1691
1692 if (quantity == 0) {
1693 var response = confirm(theme.strings.cartConfirmDelete);
1694 if (response === false) {
1695 $input.val(1);
1696 this.loading(false);
1697 return;
1698 }
1699 }
1700
1701 theme.cart.changeItem(id, quantity);
1702 },
1703
1704 _onItemQuantityEmptyBlur: function(evt) {
1705 var $input = $(evt.target);
1706 var id = $input.closest(selectors.item).attr(data.itemId);
1707 var value = $input.val();
1708
1709 if (value !== '') { return; }
1710
1711 theme.cart.getCart().then(function(cart) {
1712 this.buildCart(cart);
1713 }.bind(this));
1714 },
1715
1716 _onItemDelete: function(evt) {
1717 evt.preventDefault();
1718
1719 var $deleteButton = $(evt.target);
1720 var $items = $(selectors.item, this.$container);
1721 var $item = $deleteButton.closest(selectors.item);
1722 var $note = $(selectors.cartNoteContainer, this.$container);
1723 var id = $item.attr(data.itemId);
1724
1725 if ($items.length === 2 && $items.last().is($note)) {
1726 $note.addClass(classes.cartItemRemove);
1727
1728 slate.a11y.promiseTransitionEnd($(selectors.itemList, this.$container)).then(function() {
1729 $note.removeClass(classes.cartItemRemove);
1730 });
1731 }
1732
1733 $item.addClass(classes.cartItemRemove);
1734
1735 slate.a11y.promiseAnimationEnd($item).then(function() {
1736 theme.cart.changeItem(id, 0);
1737 }.bind(this));
1738 },
1739
1740 loading: function(state) {
1741 this.status.loading = state;
1742
1743 if (state) {
1744 $(selectors.itemList, this.$form).addClass('loading');
1745 } else {
1746 $(selectors.itemList, this.$form).removeClass('loading');
1747 }
1748 },
1749
1750 sizeCart: function() {
1751 // If drawer cart is set to static, measure to make sure it should
1752 // stay that way
1753 if (this.$container.hasClass(classes.staticDrawerElement)) {
1754 // Add static class, measure, and decide if we need to add it back
1755 this.$container.addClass(classes.staticDrawerElement);
1756
1757 // If cart is taller than window, remove static class so
1758 // elements become absolute and items scrollable
1759 if (this.$container.height() >= window.innerHeight) {
1760 this.$container.removeClass(classes.staticDrawerElement);
1761 }
1762 }
1763
1764 // Stop if our drawer doesn't have a fixed footer
1765 if (!this.$container.hasClass('drawer--has-fixed-footer')) {
1766 return;
1767 }
1768
1769 // Elements are reprinted regularly so selectors are not cached
1770 var $cartFooter = this.$container.find(selectors.fixedFooter).removeAttr('style');
1771 var $cartInner = this.$container.find(selectors.fixedInnerContent).removeAttr('style');
1772 var cartFooterHeight = $cartFooter.outerHeight();
1773
1774 $cartInner.css('bottom', cartFooterHeight);
1775 $cartFooter.css('height', cartFooterHeight);
1776 },
1777
1778 updateCartNotification: function(cart) {
1779 $(selectors.cartCount).text(cart.item_count);
1780 $('body').toggleClass(classes.cartHasItems, cart.item_count > 0);
1781 },
1782
1783 _highlightText: function(evt) {
1784 // Don't want the mobile tooltip to pop up
1785 if (!theme.config.isTouch) {
1786 $(evt.target).select();
1787 }
1788 }
1789 });
1790
1791 return AjaxCart;
1792 })();
1793
1794 theme.StickyCart = (function() {
1795 var config = {
1796 namespace: '.ajaxcart'
1797 };
1798
1799 var selectors = {
1800 cart: '#StickyCart',
1801 items: '#StickyItems',
1802 subtotal: '#StickySubtotal',
1803 submit: '#StickySubmit'
1804 };
1805
1806 var classes = {
1807 cartTemplate: 'template-cart',
1808 active: 'sticky-cart--open',
1809 activeBodyClass: 'body--sticky-cart-open'
1810 };
1811
1812 function StickyCart() {
1813 this.status = {
1814 loaded: false,
1815 loading: false,
1816 open: $('body').hasClass(classes.activeBodyClass)
1817 };
1818
1819 this.initEventListeners();
1820 };
1821
1822 function refresh(cart) {
1823 if ($('body').hasClass(classes.cartTemplate)) {
1824 return;
1825 }
1826
1827 if (cart.item_count > 0) {
1828 $('body').addClass(classes.activeBodyClass);
1829 $(selectors.cart).addClass(classes.active);
1830 } else {
1831 $('body').removeClass(classes.activeBodyClass);
1832 $(selectors.cart).removeClass(classes.active);
1833 }
1834
1835 $(selectors.items).text(theme.strings.cartItems.replace('[count]', cart.item_count));
1836 $(selectors.subtotal).html(theme.Currency.formatMoney(cart.total_price, theme.settings.moneyFormat));
1837
1838 if (theme.settings.currenciesEnabled) {
1839 theme.currencySwitcher.ajaxrefresh();
1840 }
1841 };
1842
1843 StickyCart.prototype = $.extend({}, StickyCart.prototype, {
1844 initEventListeners: function() {
1845 $(selectors.submit).on('click', function() {
1846 $(this).addClass('btn--loading');
1847 });
1848
1849 $('body').on('added.ajaxProduct', function() {
1850 this.hideCart();
1851 theme.cart.getCart().then(function(cart) {
1852 this.buildCart(cart, true);
1853 }.bind(this));
1854 }.bind(this));
1855 },
1856
1857 hideCart: function() {
1858 $('body').removeClass(classes.activeBodyClass);
1859 $(selectors.cart).removeClass(classes.active);
1860 },
1861
1862 showCart: function(count, subtotal) {
1863 if (count) {
1864 $(selectors.items).text(theme.strings.cartItems.replace('[count]', count));
1865 }
1866 if (subtotal) {
1867 $(selectors.subtotal).html(theme.Currency.formatMoney(subtotal, theme.settings.moneyFormat));
1868 }
1869
1870 $('body').addClass(classes.activeBodyClass);
1871 $(selectors.cart).addClass(classes.active);
1872
1873 this.status.open = true;
1874
1875 if (theme.settings.currenciesEnabled) {
1876 theme.currencySwitcher.ajaxrefresh();
1877 }
1878 },
1879
1880 buildCart: function(cart, open) {
1881 this.loading(true);
1882
1883 this.status.loaded = true;
1884 this.loading(false);
1885
1886 // If specifically asked, open the cart (only happens after product added from form)
1887 if (open === true) {
1888 this.showCart(cart.item_count, cart.total_price);
1889 }
1890 },
1891
1892 loading: function(state) {
1893 this.status.loading = state;
1894
1895 if (state) {
1896 $(selectors.cart).addClass('is-loading');
1897 } else {
1898 $(selectors.cart).removeClass('is-loading');
1899 }
1900 },
1901
1902 updateError: function(XMLHttpRequest) {
1903 if (XMLHttpRequest.responseJSON && XMLHttpRequest.responseJSON.description) {
1904 console.warn(XMLHttpRequest.responseJSON.description);
1905 }
1906 }
1907 });
1908
1909 return {
1910 init: StickyCart,
1911 refresh: refresh
1912 }
1913 })();
1914
1915 theme.AjaxProduct = (function() {
1916 var status = {
1917 loading: false
1918 };
1919
1920 function ProductForm($form) {
1921 this.$form = $form;
1922 this.$addToCart = this.$form.find('.add-to-cart');
1923 this.productId = $form.find('[name="data-product-id"]').val();
1924
1925 if (this.$form.length) {
1926 this.$form.on('submit', this.addItemFromForm.bind(this));
1927 }
1928 };
1929
1930 ProductForm.prototype = $.extend({}, ProductForm.prototype, {
1931 addItemFromForm: function(evt, callback){
1932 evt.preventDefault();
1933
1934 if (status.loading) {
1935 return;
1936 }
1937
1938 this.$form.find('[data-add-to-cart]').addClass('btn--loading');
1939
1940 status.loading = true;
1941
1942 var data = this.$form.serialize();
1943
1944 $('body').trigger('added.ProductScreen-' + this.productId);
1945
1946 theme.cart.addItemFromForm(data)
1947 .then(function(product) {
1948 this.success(product);
1949 }.bind(this))
1950 .catch(function(XMLHttpRequest) {
1951 this.error(XMLHttpRequest)
1952 }.bind(this))
1953 .always(function() {
1954 status.loading = false;
1955 this.$form.find('[data-add-to-cart]').removeClass('btn--loading');
1956 }.bind(this));
1957 },
1958
1959 success: function(product) {
1960 this.$form.find('.errors').remove();
1961 $('body').trigger('added.ajaxProduct');
1962 document.dispatchEvent(new CustomEvent('added:ajaxProduct', {
1963 detail: {
1964 product: product
1965 }
1966 }));
1967 },
1968
1969 error: function(XMLHttpRequest) {
1970 this.$form.find('.errors').remove();
1971
1972 if (XMLHttpRequest.responseJSON && XMLHttpRequest.responseJSON.description) {
1973 console.warn(XMLHttpRequest.responseJSON.description);
1974
1975 $('body').trigger('error.ProductScreen-' + this.productId);
1976
1977 document.dispatchEvent(new CustomEvent('error:ajaxProduct', {
1978 detail: {
1979 errorMessage: XMLHttpRequest.responseJSON.description
1980 }
1981 }));
1982
1983 this.$form.prepend('<div class="errors text-center">' + XMLHttpRequest.responseJSON.description + '</div>');
1984 }
1985 }
1986 });
1987
1988 return ProductForm;
1989 })();
1990
1991 // Either collapsible containers all acting individually,
1992 // or tabs that can only have one open at a time
1993 theme.collapsibles = (function() {
1994
1995 var selectors = {
1996 trigger: '.collapsible-trigger',
1997 module: '.collapsible-content',
1998 moduleInner: '.collapsible-content__inner',
1999 tabs: '.collapsible-trigger--tab'
2000 };
2001
2002 var classes = {
2003 hide: 'hide',
2004 open: 'is-open',
2005 autoHeight: 'collapsible--auto-height',
2006 tabs: 'collapsible-trigger--tab'
2007 };
2008
2009 var namespace = '.collapsible';
2010
2011 var isTransitioning = false;
2012
2013 function init() {
2014 $(selectors.trigger).each(function() {
2015 var $el = $(this);
2016 var state = $el.hasClass(classes.open);
2017 $el.attr('aria-expanded', state);
2018 });
2019
2020 $('body')
2021 .off(namespace)
2022 .on('click' + namespace, selectors.trigger, function() {
2023 if (isTransitioning) {
2024 return;
2025 }
2026
2027 isTransitioning = true;
2028
2029 var $el = $(this);
2030 var isOpen = $el.hasClass(classes.open);
2031 var isTab = $el.hasClass(classes.tabs);
2032 var moduleId = $el.attr('aria-controls');
2033 var $module = $('#' + moduleId);
2034 var height = $module.find(selectors.moduleInner).outerHeight();
2035 var isAutoHeight = $el.hasClass(classes.autoHeight);
2036
2037 if (isTab) {
2038 if (isOpen) {
2039 isTransitioning = false;
2040 return;
2041 }
2042
2043 var $newModule;
2044 // If tab, close all other tabs with same ID before opening
2045 $(selectors.tabs + '[data-id=' + $el.data('id') + ']').each(function() {
2046 $(this).removeClass(classes.open);
2047 $newModule = $('#' + $(this).attr('aria-controls'));
2048 setTransitionHeight($newModule, 0, true);
2049 });
2050 }
2051
2052 // If isAutoHeight, set the height to 0 just after setting the actual height
2053 // so the closing animation works nicely
2054 if (isOpen && isAutoHeight) {
2055 setTimeout(function() {
2056 height = 0;
2057 setTransitionHeight($module, height, isOpen, isAutoHeight);
2058 }, 0);
2059 }
2060
2061 if (isOpen && !isAutoHeight) {
2062 height = 0;
2063 }
2064
2065 $el
2066 .attr('aria-expanded', !isOpen)
2067 .toggleClass(classes.open, !isOpen);
2068
2069 setTransitionHeight($module, height, isOpen, isAutoHeight);
2070 });
2071 }
2072
2073 function setTransitionHeight($module, height, isOpen, isAutoHeight) {
2074 $module
2075 .removeClass(classes.hide)
2076 .prepareTransition()
2077 .css('height', height)
2078 .toggleClass(classes.open, !isOpen);
2079
2080 if (!isOpen && isAutoHeight) {
2081 var o = $module;
2082 window.setTimeout(function() {
2083 o.css('height','auto');
2084 isTransitioning = false;
2085 }, 0);
2086 } else {
2087 isTransitioning = false;
2088 }
2089 }
2090
2091 return {
2092 init: init
2093 };
2094 })();
2095
2096 theme.headerNav = (function() {
2097
2098 var selectors = {
2099 wrapper: '.header-wrapper',
2100 siteHeader: '.site-header',
2101 logoContainer: '.site-header__logo',
2102 logo: '.site-header__logo img',
2103 navigation: '.site-navigation',
2104 navContainerWithLogo: '.header-item--logo',
2105 navItems: '.site-nav__item',
2106 navLinks: '.site-nav__link',
2107 navLinksWithDropdown: '.site-nav__link--has-dropdown',
2108 navDropdownLinks: '.site-nav__dropdown-link--second-level',
2109 thumbMenu: '.site-nav__thumb-menu'
2110 };
2111
2112 var classes = {
2113 hasDropdownClass: 'site-nav--has-dropdown',
2114 hasSubDropdownClass: 'site-nav__deep-dropdown-trigger',
2115 dropdownActive: 'is-focused',
2116 stickyCartActive: 'body--sticky-cart-open',
2117 overlayEnabledClass: 'header-wrapper--overlay',
2118 overlayedClass: 'is-light',
2119 thumbMenuInactive: 'site-nav__thumb-menu--inactive',
2120 stickyClass: 'site-header--sticky',
2121 overlayStickyClass: 'header-wrapper--sticky',
2122 openTransitionClass: 'site-header--opening'
2123 };
2124
2125 var config = {
2126 namespace: '.siteNav',
2127 overlayHeader: false,
2128 stickyActive: false,
2129 forceStickyOnMobile: false,
2130 forceCloseThumbNav: false
2131 };
2132
2133 // Elements used in resize functions, defined in init
2134 var $window;
2135 var $navContainerWithLogo;
2136 var $logoContainer;
2137 var $nav;
2138 var $wrapper;
2139 var $siteHeader;
2140
2141 function init() {
2142 $window = $(window);
2143 $navContainerWithLogo = $(selectors.navContainerWithLogo);
2144 $logoContainer = $(selectors.logoContainer);
2145 $nav = $(selectors.navigation);
2146 $wrapper = $(selectors.wrapper);
2147 $siteHeader = $(selectors.siteHeader);
2148
2149 config.overlayHeader = theme.settings.overlayHeader = $siteHeader.data('overlay');
2150
2151 accessibleDropdowns();
2152 var searchModal = new theme.Modals('SearchModal', 'search-modal', {
2153 closeOffContentClick: false,
2154 focusOnOpen: '#SearchModalInput'
2155 });
2156
2157 // One listener for all header-related resize and load functions
2158 $window
2159 .on('resize' + config.namespace, $.debounce(150, headerResize))
2160 .on('load' + config.namespace, headerLoad);
2161
2162 // Determine type of header:
2163 // desktop: sticky bar | sticky button | top only
2164 // mobile: always sticky button
2165 setHeaderStyle();
2166
2167 // Sticky menu (bar or thumb) on scroll
2168 $window.on('scroll' + config.namespace, $.throttle(150, stickyMenuOnScroll));
2169
2170 // Make sure sticky nav appears after header is reloaded in editor
2171 if (Shopify.designMode) {
2172 $window.trigger('resize');
2173 }
2174 }
2175
2176 function headerLoad() {
2177 resizeLogo();
2178 initStickyThumbMenu();
2179
2180 if (config.headerStyle === 'bar') {
2181 initStickyBarMenu();
2182 }
2183 }
2184
2185 function headerResize() {
2186 resizeLogo();
2187 setHeaderStyle();
2188
2189 if (config.headerStyle === 'bar') {
2190 initStickyBarMenu();
2191 }
2192 }
2193
2194 function setHeaderStyle() {
2195 if (theme.config.bpSmall) {
2196 config.headerStyle = 'button';
2197 } else {
2198 config.headerStyle = $wrapper.data('header-style');
2199 }
2200
2201 config.stickyThreshold = config.headerStyle === 'button' ? 100 : 250;
2202
2203 if (config.headerStyle !== 'button') {
2204 toggleThumbMenu(false);
2205 }
2206 }
2207
2208 function unload() {
2209 $(window).off(config.namespace);
2210 $(selectors.navLinks).off(config.namespace);
2211 $(selectors.navDropdownLinks).off(config.namespace);
2212 }
2213
2214 function resizeLogo() {
2215 // Using .each() because of possible reversed color logo
2216 $(selectors.logo).each(function() {
2217 var $el = $(this),
2218 logoWidthOnScreen = $el.width(),
2219 containerWidth = $el.closest('.grid__item').width();
2220 // If image exceeds container, let's make it smaller
2221 if (logoWidthOnScreen > containerWidth) {
2222 $el.css('maxWidth', containerWidth);
2223 }
2224 else {
2225 $el.removeAttr('style');
2226 }
2227 });
2228 }
2229
2230 function accessibleDropdowns() {
2231 var hasActiveDropdown = false;
2232 var hasActiveSubDropdown = false;
2233 var closeOnClickActive = false;
2234
2235 // Touch devices open dropdown on first click, navigate to link on second
2236 if (theme.config.isTouch) {
2237 $(selectors.navLinksWithDropdown).on('touchend' + config.namespace, function(evt) {
2238 var $el = $(this);
2239 var $parentItem = $el.parent();
2240 if (!$parentItem.hasClass(classes.dropdownActive)) {
2241 evt.preventDefault();
2242 closeDropdowns();
2243 openFirstLevelDropdown($el);
2244 } else {
2245 window.location.replace($el.attr('href'));
2246 }
2247 });
2248
2249 $(selectors.navDropdownLinks).on('touchend' + config.namespace, function(evt) {
2250 var $el = $(this);
2251 var $parentItem = $el.parent();
2252
2253 // Open third level menu or go to link based on active state
2254 if ($parentItem.hasClass(classes.hasSubDropdownClass)) {
2255 if (!$parentItem.hasClass(classes.dropdownActive)) {
2256 evt.preventDefault();
2257 closeThirdLevelDropdown();
2258 openSecondLevelDropdown($el);
2259 } else {
2260 window.location.replace($el.attr('href'));
2261 }
2262 } else {
2263 // No third level nav, go to link
2264 window.location.replace($el.attr('href'));
2265 }
2266 });
2267 }
2268
2269 // Open/hide top level dropdowns
2270 $(selectors.navLinks).on('focusin mouseover' + config.namespace, function() {
2271 if (hasActiveDropdown) {
2272 closeSecondLevelDropdown();
2273 }
2274
2275 if (hasActiveSubDropdown) {
2276 closeThirdLevelDropdown();
2277 }
2278
2279 openFirstLevelDropdown($(this));
2280 });
2281
2282 // Force remove focus on sitenav links because focus sometimes gets stuck
2283 $(selectors.navLinks).on('mouseleave' + config.namespace, function() {
2284 closeDropdowns();
2285 });
2286
2287 // Open/hide sub level dropdowns
2288 $(selectors.navDropdownLinks).on('focusin' + config.namespace, function() {
2289 closeThirdLevelDropdown();
2290 openSecondLevelDropdown($(this), true);
2291 });
2292
2293 // Private dropdown methods
2294 function openFirstLevelDropdown($el) {
2295 var $parentItem = $el.parent();
2296 if ($parentItem.hasClass(classes.hasDropdownClass)) {
2297 $parentItem.addClass(classes.dropdownActive);
2298 hasActiveDropdown = true;
2299 }
2300
2301 if (!theme.config.isTouch) {
2302 if (!closeOnClickActive) {
2303 var eventType = theme.config.isTouch ? 'touchend' : 'click';
2304 closeOnClickActive = true;
2305 $('body').on(eventType + config.namespace, function() {
2306 closeDropdowns();
2307 $('body').off(config.namespace);
2308 closeOnClickActive = false;
2309 });
2310 }
2311 }
2312 }
2313
2314 function openSecondLevelDropdown($el, skipCheck) {
2315 var $parentItem = $el.parent();
2316 if ($parentItem.hasClass(classes.hasSubDropdownClass) || skipCheck) {
2317 $parentItem.addClass(classes.dropdownActive);
2318 hasActiveSubDropdown = true;
2319 }
2320 }
2321
2322 function closeDropdowns() {
2323 closeSecondLevelDropdown();
2324 closeThirdLevelDropdown();
2325 }
2326
2327 function closeSecondLevelDropdown() {
2328 $(selectors.navItems).removeClass(classes.dropdownActive);
2329 }
2330
2331 function closeThirdLevelDropdown() {
2332 $(selectors.navDropdownLinks).parent().removeClass(classes.dropdownActive);
2333 }
2334 }
2335
2336 function initStickyBarMenu() {
2337 $siteHeader.wrap('<div class="site-header-sticky"></div>');
2338
2339 // No need to set a height on wrapper if positioned absolutely already
2340 if (config.overlayHeader) {
2341 return;
2342 }
2343
2344 stickyHeaderHeight();
2345 setTimeout(function() {
2346 stickyHeaderHeight();
2347
2348 // Don't let height get stuck on 0
2349 if ($('.site-header-sticky').outerHeight() === 0) {
2350 setTimeout(function() {
2351 $window.trigger('resize');
2352 }, 500);
2353 }
2354 }, 200);
2355
2356 $window.on('resize' + config.namespace, $.debounce(50, stickyHeaderHeight));
2357 }
2358
2359 function stickyHeaderHeight() {
2360 $('.site-header-sticky').css('height', $siteHeader.outerHeight(true));
2361 }
2362
2363 function initStickyThumbMenu() {
2364 if ($('body').hasClass(classes.stickyCartActive)) {
2365 return;
2366 }
2367
2368 if (theme.config.bpSmall && theme.template !== 'product') {
2369 setTimeout(function() {
2370 config.forceStickyOnMobile = true;
2371 toggleThumbMenu(true);
2372 }, 25);
2373 }
2374 }
2375
2376 function stickyMenuOnScroll(evt) {
2377 var scroll = $window.scrollTop();
2378
2379 if (scroll > config.stickyThreshold) {
2380 if (config.forceStickyOnMobile) {
2381 config.forceStickyOnMobile = false;
2382 }
2383
2384 if (config.stickyActive) {
2385 return;
2386 }
2387
2388 if (config.headerStyle === 'button') {
2389 toggleThumbMenu(true);
2390 } else if (config.headerStyle === 'bar') {
2391 toggleBarMenu(true);
2392 }
2393 } else {
2394 // If menu is shown on mobile page load, do not
2395 // automatically hide it when you start scrolling
2396 if (config.forceStickyOnMobile) {
2397 return;
2398 }
2399
2400 if (!config.stickyActive) {
2401 return;
2402 }
2403
2404 if (config.headerStyle === 'button') {
2405 if (!theme.config.bpSmall) {
2406 toggleThumbMenu(false);
2407 }
2408 } else if (config.headerStyle === 'bar') {
2409 toggleBarMenu(false);
2410 }
2411 }
2412 }
2413
2414 function toggleThumbMenu(active, forceClose) {
2415 // If forced close, will not open again until page refreshes
2416 // because sticky nav is open
2417 if (config.forceCloseThumbNav) {
2418 return;
2419 }
2420
2421 // If thumb menu is open, do not hide menu button
2422 if ($('.slide-nav__overflow--thumb').hasClass('js-menu--is-open')) {
2423 return;
2424 }
2425
2426 $(selectors.thumbMenu).toggleClass(classes.thumbMenuInactive, !active);
2427 config.stickyActive = active;
2428
2429 config.forceCloseThumbNav = forceClose;
2430 }
2431
2432 function toggleBarMenu(active) {
2433 if (config.headerStyle !== 'bar') {
2434 return;
2435 }
2436
2437 if (active) {
2438 $siteHeader.addClass(classes.stickyClass);
2439 if (config.overlayHeader) {
2440 $wrapper
2441 .removeClass(classes.overlayedClass)
2442 .addClass(classes.overlayStickyClass);
2443 }
2444
2445 // Add open transition class after element is set to fixed
2446 // so CSS animation is applied correctly
2447 setTimeout(function() {
2448 $siteHeader.addClass(classes.openTransitionClass);
2449 }, 100);
2450 } else {
2451 $siteHeader.removeClass(classes.openTransitionClass).removeClass(classes.stickyClass);
2452
2453 if (config.overlayHeader) {
2454 $wrapper
2455 .addClass(classes.overlayedClass)
2456 .removeClass(classes.overlayStickyClass);
2457 }
2458 }
2459
2460 config.stickyActive = active;
2461 }
2462
2463 // If the header setting to overlay the menu on the collection image
2464 // is enabled but the collection setting is disabled, we need to undo
2465 // the init of the sticky nav
2466 function disableOverlayHeader() {
2467 $(selectors.wrapper)
2468 .removeClass(classes.overlayEnabledClass)
2469 .removeClass(classes.overlayedClass);
2470 }
2471
2472 return {
2473 init: init,
2474 disableOverlayHeader: disableOverlayHeader,
2475 toggleThumbMenu: toggleThumbMenu,
2476 unload: unload
2477 };
2478 })();
2479
2480 theme.slideNav = (function() {
2481
2482 var selectors = {
2483 container: '#PageContainer',
2484 navWrapper: '.slide-nav__overflow',
2485 nav: '#SlideNav',
2486 toggleBtn: '.js-toggle-slide-nav',
2487 subNavToggleBtn: '.js-toggle-submenu',
2488 thumbNavToggle: '.site-nav__thumb-button'
2489 };
2490
2491 var classes = {
2492 subNavLink: 'slide-nav__sublist-link',
2493 return: 'slide-nav__return-btn',
2494 isActive: 'is-active',
2495 isOpen: 'js-menu--is-open',
2496 subNavShowing: 'sub-nav--is-open',
2497 thirdNavShowing: 'third-nav--is-open'
2498 };
2499
2500 var namespace = '.slideNav';
2501
2502 var isTransitioning;
2503 var $activeSubNav;
2504 var $activeTrigger;
2505 var pageSlide = true;
2506 var menuLevel = 1;
2507
2508 function init() {
2509 if ($(selectors.thumbNavToggle).length) {
2510 pageSlide = false;
2511 }
2512
2513 $(selectors.toggleBtn).on('click' + namespace, toggleNav);
2514 $(selectors.subNavToggleBtn).on('click' + namespace, toggleSubNav);
2515 }
2516
2517 function toggleNav() {
2518 if ($(selectors.toggleBtn).hasClass(classes.isActive)) {
2519 closeNav();
2520 } else {
2521 openNav();
2522 }
2523 }
2524
2525 function openNav() {
2526 $(selectors.toggleBtn).addClass(classes.isActive);
2527
2528 $(selectors.navWrapper).prepareTransition().addClass(classes.isOpen);
2529
2530 if (pageSlide) {
2531 $(selectors.container).css({
2532 transform:
2533 'translate3d(0, ' + $(selectors.navWrapper).height() + 'px, 0)'
2534 });
2535 }
2536
2537 $(selectors.navWrapper).attr('tabindex', '-1').focus();
2538
2539 // close on escape
2540 $(window).on('keyup' + namespace, function(evt) {
2541 if (evt.which === 27) {
2542 closeNav();
2543 }
2544 });
2545 }
2546
2547 function closeNav() {
2548 $(selectors.toggleBtn).removeClass(classes.isActive);
2549 $(selectors.navWrapper).prepareTransition().removeClass(classes.isOpen);
2550
2551 if (pageSlide) {
2552 $(selectors.container).removeAttr('style');
2553 }
2554
2555 $(selectors.toggleBtn).focus();
2556
2557 $(window).off('keyup' + namespace);
2558 }
2559
2560 function toggleSubNav(evt) {
2561 if (isTransitioning) {
2562 return;
2563 }
2564
2565 var $toggleBtn = $(evt.currentTarget);
2566 var isReturn = $toggleBtn.hasClass(classes.return);
2567 isTransitioning = true;
2568
2569 if (isReturn) {
2570 // Close all subnavs by removing active class on buttons
2571 $(
2572 classes.toggleBtn + '[data-level="' + (menuLevel - 1) + '"]'
2573 ).removeClass(classes.isActive);
2574 $('.slide-nav__dropdown[data-level="' + (menuLevel) + '"]').prepareTransition().removeClass(classes.isActive);
2575
2576 if ($activeTrigger && $activeTrigger.length) {
2577 $activeTrigger.removeClass(classes.isActive);
2578 }
2579 } else {
2580 $toggleBtn.addClass(classes.isActive);
2581 $toggleBtn.next('.slide-nav__dropdown').prepareTransition().addClass(classes.isActive);
2582 }
2583
2584 $activeTrigger = $toggleBtn;
2585
2586 goToSubnav($toggleBtn.data('target'));
2587 }
2588
2589 function goToSubnav(target) {
2590 var $targetMenu = target
2591 ? $('.slide-nav__dropdown[data-parent="' + target + '"]')
2592 : $(selectors.nav);
2593
2594 menuLevel = $targetMenu.data('level') ? $targetMenu.data('level') : 1;
2595
2596 $activeSubNav = $targetMenu;
2597
2598 var $elementToFocus = target
2599 ? $targetMenu.find('.' + classes.subNavLink + ':first')
2600 : $activeTrigger;
2601
2602 var translateMenuHeight = $targetMenu.outerHeight();
2603
2604 var openNavClass =
2605 menuLevel > 2 ? classes.thirdNavShowing : classes.subNavShowing;
2606
2607 $(selectors.navWrapper)
2608 .css('height', translateMenuHeight)
2609 .removeClass(classes.thirdNavShowing)
2610 .addClass(openNavClass);
2611
2612 if (!target) {
2613 // Show top level nav
2614 $(selectors.navWrapper)
2615 .removeClass(classes.thirdNavShowing)
2616 .removeClass(classes.subNavShowing);
2617 }
2618
2619 isTransitioning = false;
2620
2621 // Match height of subnav
2622 if (pageSlide) {
2623 $(selectors.container).css({
2624 transform: 'translate3d(0, ' + translateMenuHeight + 'px, 0)'
2625 });
2626 }
2627 }
2628
2629 function unload() {
2630 $(window).off(namespace);
2631 $(selectors.toggleBtn).off(namespace);
2632 $(selectors.subNavToggleBtn).off(namespace);
2633 }
2634
2635 return {
2636 init: init,
2637 unload: unload
2638 };
2639 })();
2640
2641 theme.articleImages = (function() {
2642
2643 var cache = {};
2644
2645 function init() {
2646 cache.$rteImages = $('.rte--indented-images');
2647
2648 if (!cache.$rteImages.length) {
2649 return;
2650 }
2651
2652 $(window).on('load', setImages);
2653 }
2654
2655 function setImages() {
2656 cache.$rteImages.find('img').each(function() {
2657 var $el = $(this);
2658 var attr = $el.attr('style');
2659
2660 // Check if undefined or float: none
2661 if (!attr || attr == 'float: none;') {
2662 // Remove grid-breaking styles if image isn't wider than parent
2663 if ($el.width() < cache.$rteImages.width()) {
2664 $el.addClass('rte__no-indent');
2665 }
2666 }
2667 });
2668 }
2669
2670 return {
2671 init: init
2672 };
2673 })();
2674
2675 theme.Slideshow = (function() {
2676 this.$slideshow = null;
2677
2678 var classes = {
2679 next: 'is-next',
2680 init: 'is-init',
2681 wrapper: 'slideshow-wrapper',
2682 slideshow: 'slideshow',
2683 currentSlide: 'slick-current',
2684 pauseButton: 'slideshow__pause',
2685 isPaused: 'is-paused'
2686 };
2687
2688 function slideshow(el, args) {
2689 this.$slideshow = $(el);
2690 this.$wrapper = this.$slideshow.closest('.' + classes.wrapper);
2691 this.$pause = this.$wrapper.find('.' + classes.pauseButton);
2692
2693 this.settings = {
2694 accessibility: true,
2695 arrows: args.arrows ? true : false,
2696 dots: args.dots ? true : false,
2697 draggable: true,
2698 touchThreshold: 8,
2699 speed: 300,
2700 pauseOnHover: args.pauseOnHover ? true : false,
2701 autoplay: this.$slideshow.data('autoplay'),
2702 autoplaySpeed: this.$slideshow.data('speed')
2703 };
2704
2705 this.$slideshow.on('init', this.init.bind(this));
2706
2707 // Refresh main page slideshow
2708 if ($('.root').find(this.$slideshow).length) {
2709 $('body').on('productModalClose', function() {
2710 this.$slideshow.addClass('slideshow-refresh');
2711 this.$slideshow.slick('refresh');
2712 }.bind(this));
2713 }
2714
2715 this.$slideshow.slick(this.settings);
2716
2717 this.$pause.on('click', this._togglePause.bind(this));
2718 }
2719
2720 slideshow.prototype = $.extend({}, slideshow.prototype, {
2721 init: function(event, obj) {
2722 this.$slideshowList = obj.$list;
2723 this.$slickDots = obj.$dots;
2724 this.$allSlides = obj.$slides;
2725 this.slideCount = obj.slideCount;
2726
2727 this.$slideshow.addClass(classes.init);
2728 this._a11y();
2729 this._clonedLazyloading();
2730 },
2731 destroy: function() {
2732 this.$slideshow.slick('unslick');
2733 },
2734
2735 // Playback
2736 _play: function() {
2737 this.$slideshow.slick('slickPause');
2738 $(classes.pauseButton).addClass('is-paused');
2739 },
2740 _pause: function() {
2741 this.$slideshow.slick('slickPlay');
2742 $(classes.pauseButton).removeClass('is-paused');
2743 },
2744 _togglePause: function() {
2745 var slideshowSelector = this._getSlideshowId(this.$pause);
2746 if (this.$pause.hasClass(classes.isPaused)) {
2747 this.$pause.removeClass(classes.isPaused);
2748 $(slideshowSelector).slick('slickPlay');
2749 } else {
2750 this.$pause.addClass(classes.isPaused);
2751 $(slideshowSelector).slick('slickPause');
2752 }
2753 },
2754
2755 // Helpers
2756 _getSlideshowId: function($el) {
2757 return '#Slideshow-' + $el.data('id');
2758 },
2759 _activeSlide: function() {
2760 return this.$slideshow.find('.slick-active');
2761 },
2762 _currentSlide: function() {
2763 return this.$slideshow.find('.slick-current');
2764 },
2765 _nextSlide: function(index) {
2766 return this.$slideshow.find('.slideshow__slide[data-slick-index="' + index + '"]');
2767 },
2768
2769 // a11y fixes
2770 _a11y: function() {
2771 var $list = this.$slideshowList;
2772 var autoplay = this.settings.autoplay;
2773
2774 if (!$list) {
2775 return;
2776 }
2777
2778 // Remove default Slick aria-live attr until slider is focused
2779 $list.removeAttr('aria-live');
2780
2781 // When an element in the slider is focused
2782 // pause slideshow and set aria-live
2783 $(classes.wrapper).on('focusin', function(evt) {
2784 if (!$(classes.wrapper).has(evt.target).length) {
2785 return;
2786 }
2787
2788 $list.attr('aria-live', 'polite');
2789 if (autoplay) {
2790 this._pause();
2791 }
2792 }.bind(this));
2793
2794 // Resume autoplay
2795 $(classes.wrapper).on('focusout', function(evt) {
2796 if (!$(classes.wrapper).has(evt.target).length) {
2797 return;
2798 }
2799
2800 $list.removeAttr('aria-live');
2801 if (autoplay) {
2802 this._play();
2803 }
2804 }.bind(this));
2805 },
2806
2807 // Make sure lazyloading works on cloned slides
2808 _clonedLazyloading: function() {
2809 var $slideshow = this.$slideshow;
2810
2811 $slideshow.find('.slick-slide').each(function(index, el) {
2812 var $slide = $(el);
2813 if ($slide.hasClass('slick-cloned')) {
2814 var slideId = $slide.data('id');
2815 var $slideImg = $slide.find('.hero__image').removeClass('lazyloading').addClass('lazyloaded');
2816
2817 // Get inline style attribute from non-cloned slide with arbitrary timeout
2818 // so the image is loaded
2819 setTimeout(function() {
2820 var loadedImageStyle = $slideshow.find('.slideshow__slide--' + slideId + ':not(.slick-cloned) .hero__image').attr('style');
2821
2822 if (loadedImageStyle) {
2823 $slideImg.attr('style', loadedImageStyle);
2824 }
2825
2826 }, this.settings.autoplaySpeed / 1.5);
2827
2828 }
2829 }.bind(this));
2830 }
2831 });
2832
2833 return slideshow;
2834 })();
2835
2836 theme.currencySwitcher = (function() {
2837
2838 var selectors = {
2839 dataDiv: '#CurrencyData',
2840 currencyOptions: '.currency-options__btn',
2841 pickerFlag: '#CurrencyFlag',
2842 pickerLabel: '#CurrencyLabel'
2843 };
2844
2845 var data = {};
2846 var modal;
2847
2848 function init() {
2849 var $dataDiv = $(selectors.dataDiv);
2850
2851 if (!$dataDiv.length) {
2852 return;
2853 }
2854
2855 modal = new theme.Modals('CurrencyModal', 'currency-modal', {
2856 closeOffContentClick: false
2857 });
2858
2859 $(selectors.currencyOptions).on('click', setNewCurrency);
2860
2861 data = {
2862 currency: $dataDiv.data('shop-currency'),
2863 format: $dataDiv.data('format'),
2864 moneyFormat: $dataDiv.data('money-format'),
2865 moneyCurrencyFormat: $dataDiv.data('money-currency-format')
2866 };
2867
2868 if (!theme.settings.nativeMultiCurrency) {
2869 Currency.format = data.format;
2870
2871 // Rely on the shop's currency format, not Shopify defaults (in case merchant changes it)
2872 Currency.money_format[data.currency] = data.moneyFormat;
2873 Currency.money_with_currency_format[data.currency] = data.moneyCurrencyFormat;
2874
2875 // Fix for customer account page
2876 $('span.money span.money').each(function() {
2877 $(this).parents('span.money').removeClass('money');
2878 });
2879
2880 // Save current price
2881 $('span.money').each(function() {
2882 $(this).attr('data-currency-' + data.currency, $(this).html());
2883 });
2884
2885 checkCookie();
2886 }
2887 }
2888
2889 function setNewCurrency() {
2890 var newCurrency = $(this).data('value');
2891
2892 if (theme.settings.nativeMultiCurrency) {
2893 $(this).addClass('is-active');
2894 theme.cart.updateCurrency(newCurrency);
2895 return;
2896 }
2897
2898 if (newCurrency !== data.currency) {
2899 data.currency = newCurrency;
2900 $(selectors.dataDiv).data('current-currency', newCurrency);
2901 updatePicker(newCurrency);
2902
2903 refresh();
2904 }
2905
2906 modal.close();
2907 }
2908
2909 function updatePicker(currency) {
2910 $(selectors.pickerFlag).attr('data-flag', currency);
2911 $(selectors.pickerLabel).text(currency);
2912
2913 // Update modal options active states
2914 $(selectors.currencyOptions).removeClass('is-active');
2915 $(selectors.currencyOptions + '[data-value=' + currency + ']').addClass('is-active');
2916 }
2917
2918 // Refresh functions only needed when not using native multi-currency
2919 function refresh() {
2920 if (theme.settings.nativeMultiCurrency) {
2921 return;
2922 }
2923
2924 var newCurrency = $(selectors.dataDiv).data('current-currency');
2925 Currency.convertAll(Currency.currentCurrency, newCurrency);
2926 }
2927
2928 function ajaxrefresh() {
2929 if (theme.settings.nativeMultiCurrency) {
2930 return;
2931 }
2932
2933 var shopCurrency = $(selectors.dataDiv).data('shop-currency');
2934 var newCurrency = $(selectors.dataDiv).data('current-currency');
2935 // Ajax cart always returns shop's currency, not what theme settings defines
2936 Currency.convertAll(shopCurrency, newCurrency);
2937 }
2938
2939 function checkCookie() {
2940 var cookieCurrency = Currency.cookie.read();
2941
2942 if (cookieCurrency == null) {
2943 Currency.currentCurrency = cookieCurrency = data.currency;
2944 } else if ($(selectors.currencyOptions).length && $(selectors.currencyOptions + '[data-value=' + cookieCurrency + ']').length === 0) {
2945 // If the cookie value does not correspond to any value in the currency dropdown
2946 Currency.currentCurrency = data.currency;
2947 Currency.cookie.write(data.currency);
2948 } else if (cookieCurrency === data.currency) {
2949 Currency.currentCurrency = data.currency;
2950 } else {
2951 Currency.convertAll(data.currency, cookieCurrency);
2952 }
2953
2954 // Update current currency with cookie value
2955 $(selectors.dataDiv).data('current-currency', cookieCurrency);
2956 data.currency = cookieCurrency;
2957 updatePicker(cookieCurrency);
2958 }
2959
2960 return {
2961 init: init,
2962 refresh: refresh,
2963 ajaxrefresh: ajaxrefresh
2964 };
2965 })();
2966
2967 /*
2968 Quick shop modals, or product screens, live inside
2969 product-grid-item markup until page load, where they're
2970 moved to #ProductScreens at the bottom of the page
2971 */
2972
2973 theme.QuickShopScreens = (function() {
2974
2975 var startingUrl = window.location.pathname;
2976 var currentPath = startingUrl;
2977 var prevPath = null;
2978 var currentScreen = null;
2979 if ('scrollRestoration' in history) {
2980 history.scrollRestoration = 'manual';
2981 }
2982
2983 var selectors = {
2984 screensWrap: '#ProductScreens',
2985 screens: '[data-product-id]',
2986 trigger: '.quick-product__btn'
2987 };
2988
2989 var activeIds = [];
2990
2991 function init(container) {
2992 if (!theme.settings.quickView) {
2993 return;
2994 }
2995
2996 var productIds = getProductIds();
2997 initProductScreens(productIds);
2998 initHistoryWatcher();
2999 }
3000
3001 function initHistoryWatcher() {
3002 // No need to adjust URL in the editor since it handles the navigation
3003 if (Shopify.designMode) {
3004 return;
3005 }
3006
3007 // Listen for product screens opening
3008 $(window).on('newPopstate', function(evt, data) {
3009 currentScreen = data.screen;
3010 // Manually trigger back, comes from esc key or close btns
3011 if (data.back) {
3012 prevPath = location.pathname;
3013 currentPath = startingUrl;
3014 history.pushState({}, '', startingUrl);
3015 }
3016
3017 if (data.url) {
3018 if (data.updateCurrentPath) {
3019 prevPath = location.pathname;
3020 currentPath = data.url;
3021 history.pushState({}, '', data.url);
3022 }
3023 }
3024 });
3025
3026 $(window).on('popstate', function(evt) {
3027 var goToUrl = false;
3028 prevPath = currentPath;
3029
3030 // Hash change or no change, let browser take over
3031 if (location.pathname === currentPath) {
3032 return;
3033 }
3034
3035 prevPath = currentPath;
3036 currentPath = location.pathname;
3037
3038 // Back to where we started. Close existing screen if open
3039 if (location.pathname === startingUrl) {
3040 if (currentScreen && currentScreen.isOpen) {
3041 closeScreen(currentScreen);
3042 }
3043 return;
3044 }
3045
3046 // Opening product
3047 if (location.pathname.indexOf('/products/') !== -1) {
3048 if (currentScreen) {
3049 currentScreen.open();
3050 } else {
3051 // No screen sent to function, trigger new click
3052 $('.quick-product__btn[href="'+ location.pathname +'"]').first().trigger('click', { updateCurrentPath: false });
3053 }
3054
3055 return;
3056 }
3057
3058 if (evt.originalEvent.state) {
3059 if (currentScreen && currentScreen.isOpen) {
3060 closeScreen(currentScreen);
3061 history.replaceState({}, '', startingUrl);
3062 return;
3063 }
3064
3065 goToUrl = true;
3066 } else {
3067 if (currentScreen) {
3068 if (currentScreen.isOpen) {
3069 closeScreen(currentScreen);
3070 return;
3071 }
3072 } else {
3073 // No state/modal. Navigate to where browser wants
3074 goToUrl = true;
3075 }
3076 }
3077
3078 // Fallback if none of our conditions are met
3079 if (goToUrl) {
3080 window.location.href = location.href;
3081 }
3082 }.bind(this));
3083 }
3084
3085 function closeScreen(screen) {
3086 screen.close();
3087 currentScreen = null;
3088 $(window).trigger('resize');
3089 }
3090
3091 function getProductIds($scope) {
3092 var ids = [];
3093
3094 var $triggers = $scope ? $(selectors.trigger, $scope) : $(selectors.trigger);
3095
3096 $triggers.each(function() {
3097 var id = $(this).data('product-id');
3098
3099 // If another identical modal exists, remove from DOM
3100 if (ids.indexOf(id) > -1) {
3101 $('.screen-layer--product[data-product-id="' + id + '"]').slice(1).remove();
3102 return;
3103 }
3104
3105 ids.push(id);
3106 });
3107
3108 return ids;
3109 }
3110
3111 function getIdsFromTriggers($triggers) {
3112 var ids = [];
3113
3114 $triggers.each(function() {
3115 var id = $(this).data('product-id');
3116 ids.push(id);
3117 });
3118
3119 return ids;
3120 }
3121
3122 function initProductScreens(ids) {
3123 var screenId;
3124 var $screenLayer;
3125 var screens = [];
3126
3127 // Init screens if they're not duplicates
3128 for (var i = 0; i < ids.length; i++) {
3129 if (activeIds.indexOf(ids[i]) === -1) {
3130 screenId = 'ProductScreen-' + ids[i];
3131 $screenLayer = $('#' + screenId);
3132
3133 screens.push($screenLayer);
3134 activeIds.push(ids[i]);
3135 new theme.ProductScreen(screenId, 'product-' + ids[i]);
3136 }
3137 }
3138
3139 // Append screens to bottom of page
3140 $(selectors.screensWrap).append(screens);
3141 }
3142
3143 // Section unloaded in theme editor.
3144 // Check if product exists in any other area
3145 // of the page, remove other's section.instance
3146 function unload($container) {
3147 if (!theme.settings.quickView) {
3148 return;
3149 }
3150
3151 var removeIds = [];
3152 var productIds = getProductIds($container);
3153
3154 // Get ids from buttons not in removed section
3155 var $activeButtons = $(selectors.trigger).not($(selectors.trigger, $container));
3156 var stillActiveIds = getIdsFromTriggers($activeButtons);
3157
3158 // If ID exists on active button, do not add to IDs to remove
3159 for (var i = 0; i < productIds.length; i++) {
3160 var id = productIds[i];
3161 if (stillActiveIds.indexOf(id) === -1) {
3162 removeIds.push(id);
3163 }
3164 }
3165
3166 for (var i = 0; i < removeIds.length; i++) {
3167 sections._removeInstance(removeIds[i]);
3168 }
3169 }
3170
3171 // Section container is sent, so must re-scrape for product IDs
3172 function reInit($container) {
3173 if (!theme.settings.quickView) {
3174 return;
3175 }
3176
3177 var newProductIds = getProductIds($container);
3178 initProductScreens(newProductIds);
3179 removeDuplicateModals(newProductIds, $container);
3180
3181 // Re-register product templates in quick view modals.
3182 // Will not double-register.
3183 sections.register('product-template', theme.Product, $('#ProductScreens'));
3184 }
3185
3186 function removeDuplicateModals(ids, $container) {
3187 for (var i = 0; i < ids.length; i++) {
3188 $('.screen-layer--product[data-product-id="' + ids[i] + '"]', $container).remove();
3189 }
3190 }
3191
3192 return {
3193 init: init,
3194 unload: unload,
3195 reInit: reInit
3196 };
3197 })();
3198
3199 /*
3200 Hover to enable slideshow of product images.
3201 On mobile slideshow starts as item is in view.
3202 Destroy on mouseout/out of view.
3203 */
3204
3205 theme.HoverProductGrid = (function() {
3206 var selectors = {
3207 product: '.grid-product',
3208 slider: '.product-slider',
3209 };
3210
3211 function HoverProductGrid($container) {
3212 this.$container = $container;
3213 this.sectionId = this.$container.attr('data-section-id');
3214 this.namespace = '.product-image-slider-' + this.sectionId;
3215 this.activeIds = [];
3216
3217 if (!theme.settings.hoverProductGrid) {
3218 return;
3219 }
3220
3221 this.$products = $container.find(selectors.product);
3222 this.slidersMobile = $container.data('product-sliders-mobile');
3223
3224 // No products means no sliders
3225 if (this.$products.length === 0) {
3226 return;
3227 }
3228
3229 slate.utils.promiseStylesheet().then(function() {
3230 this.init();
3231 }.bind(this));
3232 }
3233
3234 HoverProductGrid.prototype = $.extend({}, HoverProductGrid.prototype, {
3235 init: function() {
3236 this.destroyAllSliders();
3237 this.setupEventType();
3238 this.listnerSetup();
3239 },
3240
3241 setupEventType: function() {
3242 this.$products.off('mouseenter mouseout');
3243 $(window).off('scroll' + this.namespace);
3244
3245 if (theme.config.bpSmall) {
3246 if (this.slidersMobile) {
3247 $(window).on('scroll' + this.namespace, $.throttle(120, this.inViewSliderInit.bind(this)));
3248 $(window).trigger('scroll' + this.namespace);
3249 }
3250 } else {
3251 this.mouseSliderInit();
3252 }
3253 },
3254
3255 listnerSetup: function() {
3256 $('body').on('matchSmall matchLarge', function() {
3257 this.destroyAllSliders();
3258 this.setupEventType();
3259 }.bind(this));
3260 },
3261
3262 inViewSliderInit: function() {
3263 this.$products.find(selectors.slider).each(function(i, el) {
3264 if(theme.isElementVisible($(el), -400)) {
3265 this.initSlider($(el));
3266 } else {
3267 this.destroySlider($(el));
3268 }
3269 }.bind(this));
3270 },
3271
3272 mouseSliderInit: function() {
3273 this.$products.on('mouseenter', function(evt) {
3274 var $slider = $(evt.currentTarget).find(selectors.slider);
3275 this.initSlider($slider);
3276 }.bind(this));
3277
3278 this.$products.on('mouseleave', function(evt) {
3279 var $slider = $(evt.currentTarget).find(selectors.slider);
3280 this.destroySlider($slider);
3281 }.bind(this));
3282 },
3283
3284 initSlider: function($slider) {
3285 if ($slider.data('image-count') < 2) {
3286 return;
3287 }
3288
3289 if (this.activeIds.indexOf($slider.data('id')) !== -1) {
3290 return;
3291 }
3292
3293 this.activeIds.push($slider.data('id'));
3294
3295 $slider
3296 .addClass('product-slider--init')
3297 .slick({
3298 autoplay: true,
3299 infinite: true,
3300 arrows: false,
3301 speed: 300,
3302 fade: true,
3303 pauseOnHover: false,
3304 autoplaySpeed: 1050
3305 });
3306 },
3307
3308 destroySlider: function($slider) {
3309 if ($slider.data('image-count') < 2) {
3310 return;
3311 }
3312
3313 var alreadyActive = this.activeIds.indexOf($slider.data('id'));
3314 if (alreadyActive !== -1) {
3315 this.activeIds.splice(alreadyActive, 1);
3316 $slider.slick('unslick');
3317 }
3318 },
3319
3320 destroyAllSliders: function() {
3321 this.$products.find(selectors.slider).each(function(i, el) {
3322 this.destroySlider($(el));
3323 }.bind(this));
3324 }
3325 });
3326
3327 return HoverProductGrid;
3328 })();
3329
3330 theme.videoModal = function() {
3331 var videoModalPlayer = null;
3332 var videoOptions = {
3333 width: 1280,
3334 height: 720,
3335 playerVars: {
3336 autohide: 0,
3337 autoplay: 1,
3338 branding: 0,
3339 cc_load_policy: 0,
3340 fs: 0,
3341 iv_load_policy: 3,
3342 modestbranding: 1,
3343 playsinline: 1,
3344 quality: 'hd720',
3345 rel: 0,
3346 showinfo: 0,
3347 wmode: 'opaque'
3348 }
3349 };
3350
3351 var selectors = {
3352 triggers: 'a[href*="youtube.com/watch"], a[href*="youtu.be/"]'
3353 };
3354
3355 if (!$(selectors.triggers).length) {
3356 return;
3357 }
3358
3359 var modal = new theme.Modals('VideoModal', 'video-modal', {
3360 closeOffContentClick: true
3361 });
3362
3363 $(selectors.triggers).on('click', triggerYouTubeModal);
3364
3365 function triggerYouTubeModal(evt) {
3366 evt.preventDefault();
3367 window.loadYouTube();
3368
3369 if (theme.config.youTubeReady) {
3370 startVideoOnClick(evt);
3371 } else {
3372 $('body').on('youTubeReady', function() {
3373 startVideoOnClick(evt);
3374 });
3375 }
3376 }
3377
3378 function startVideoOnClick(evt) {
3379 var $el = $(evt.target);
3380
3381 // get video ID from URL
3382 var videoId = getYoutubeVideoId($el.attr('href'));
3383
3384 var args = $.extend({}, videoOptions, {
3385 videoId: videoId
3386 });
3387
3388 // Disable plays inline on mobile
3389 args.playerVars.playsinline = theme.config.bpSmall ? 0 : 1;
3390
3391 var videoModalPlayer = new YT.Player('VideoHolder', args);
3392 modal.open();
3393
3394 $('body').on('modalClose.VideoModal', function() {
3395 // Slight timeout so it is destroyed after the modal closes
3396 setTimeout(function() {
3397 videoModalPlayer.destroy();
3398 }, 500); // modal close css transition
3399 });
3400 }
3401
3402 function getYoutubeVideoId(url) {
3403 var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
3404 var match = url.match(regExp);
3405 return (match&&match[7].length==11)? match[7] : false;
3406 }
3407 };
3408
3409
3410
3411 theme.customerTemplates = (function() {
3412
3413 function initEventListeners() {
3414 // Show reset password form
3415 $('#RecoverPassword').on('click', function(evt) {
3416 evt.preventDefault();
3417 toggleRecoverPasswordForm();
3418 });
3419
3420 // Hide reset password form
3421 $('#HideRecoverPasswordLink').on('click', function(evt) {
3422 evt.preventDefault();
3423 toggleRecoverPasswordForm();
3424 });
3425 }
3426
3427 /**
3428 *
3429 * Show/Hide recover password form
3430 *
3431 */
3432 function toggleRecoverPasswordForm() {
3433 $('#RecoverPasswordForm').toggleClass('hide');
3434 $('#CustomerLoginForm').toggleClass('hide');
3435 }
3436
3437 /**
3438 *
3439 * Show reset password success message
3440 *
3441 */
3442 function resetPasswordSuccess() {
3443 var $formState = $('.reset-password-success');
3444
3445 // check if reset password form was successfully submitted
3446 if (!$formState.length) {
3447 return;
3448 }
3449
3450 // show success message
3451 $('#ResetSuccess').removeClass('hide');
3452 }
3453
3454 /**
3455 *
3456 * Show/hide customer address forms
3457 *
3458 */
3459 function customerAddressForm() {
3460 var $newAddressForm = $('#AddressNewForm');
3461 var $addressForms = $('.js-address-form');
3462
3463 if (!$newAddressForm.length || !$addressForms.length) {
3464 return;
3465 }
3466
3467 if (Shopify) {
3468 $('.js-address-country').each(function() {
3469 var $container = $(this);
3470 var countryId = $container.data('country-id');
3471 var provinceId = $container.data('province-id');
3472 var provinceContainerId = $container.data('province-container-id');
3473
3474 new Shopify.CountryProvinceSelector(
3475 countryId,
3476 provinceId,
3477 {
3478 hideElement: provinceContainerId
3479 }
3480 );
3481 });
3482 }
3483
3484 // Toggle new/edit address forms
3485 $('.address-new-toggle').on('click', function() {
3486 $newAddressForm.toggleClass('hide');
3487 });
3488
3489 $('.address-edit-toggle').on('click', function() {
3490 var formId = $(this).data('form-id');
3491 $('#EditAddress_' + formId).toggleClass('hide');
3492 });
3493
3494 $('.address-delete').on('click', function() {
3495 var $el = $(this);
3496 var formId = $el.data('form-id');
3497 var confirmMessage = $el.data('confirm-message');
3498
3499 if (confirm(confirmMessage || 'Are you sure you wish to delete this address?')) {
3500 Shopify.postLink('/account/addresses/' + formId, {parameters: {_method: 'delete'}});
3501 }
3502 });
3503 }
3504
3505 /**
3506 *
3507 * Check URL for reset password hash
3508 *
3509 */
3510 function checkUrlHash() {
3511 var hash = window.location.hash;
3512
3513 // Allow deep linking to recover password form
3514 if (hash === '#recover') {
3515 toggleRecoverPasswordForm();
3516 }
3517 }
3518
3519 return {
3520 init: function() {
3521 checkUrlHash();
3522 initEventListeners();
3523 resetPasswordSuccess();
3524 customerAddressForm();
3525 }
3526 };
3527 })();
3528
3529
3530 theme.Product = (function() {
3531
3532 var classes = {
3533 onSale: 'sale-price',
3534 disabled: 'disabled',
3535 isModal: 'is-modal',
3536 loading: 'loading',
3537 loaded: 'loaded',
3538 interactable: 'video-interactable',
3539 visuallyHide: 'visually-invisible',
3540 thumbActive: 'thumb--current'
3541 };
3542
3543 var selectors = {
3544 variantsJson: '[data-variant-json]',
3545 currentVariantJson: '[data-current-variant-json]',
3546
3547 imageContainer: '[data-product-images]',
3548 mainSlider: '[data-product-photos]',
3549 thumbSlider: '[data-product-thumbs]',
3550 photo: '[data-product-photo]',
3551 photoThumbs: '[data-product-thumb]',
3552 photoThumbItem: '[data-product-thumb-item]',
3553
3554 priceWrapper: '[data-price-wrapper]',
3555 price: '[data-product-price]',
3556 comparePrice: '[data-product-price-compare]',
3557 priceA11y: '[data-price-a11y]',
3558 comparePriceA11y: '[data-compare-a11y]',
3559 sku: '[data-sku]',
3560 inventory: '[data-product-inventory]',
3561 incomingInventory: '[data-product-incoming-inventory]',
3562 unitWrapper: '[data-product-unit-wrapper]',
3563
3564 addToCart: '[data-add-to-cart]',
3565 addToCartText: '[data-add-to-cart-text]',
3566
3567 originalSelectorId: '[data-product-select]',
3568 singleOptionSelector: '[data-variant-input]',
3569 variantColorSwatch: '[data-color-swatch]',
3570
3571 productImageMain: '.product-image-main',
3572 productVideo: '[data-product-video]',
3573 videoParent: '.product__video-wrapper',
3574 currentSlide: '.slick-current',
3575
3576 modalFormHolder: '#ProductFormPlaceholder-',
3577 formContainer: '.product-single__form'
3578 };
3579
3580 var youtubeReady;
3581 var videos = {};
3582 var youtubePlayers = [];
3583 var youtubeVideoOptions = {
3584 height: '480',
3585 width: '850',
3586 playerVars :{
3587 autohide: 0,
3588 autoplay: 1,
3589 branding: 0,
3590 cc_load_policy: 0,
3591 controls: 0,
3592 fs: 0,
3593 iv_load_policy: 3,
3594 modestbranding: 1,
3595 playsinline: 1,
3596 quality: 'hd720',
3597 rel: 0,
3598 showinfo: 0,
3599 wmode: 'opaque'
3600 },
3601 events: {
3602 onReady: onVideoPlayerReady,
3603 onStateChange: onVideoStateChange
3604 }
3605 };
3606
3607 var vimeoReady;
3608 var vimeoPlayers = [];
3609 var vimeoVideoOptions = {
3610 byline: false,
3611 title: false,
3612 portrait: false,
3613 loop: true
3614 };
3615
3616 function onVideoPlayerReady(evt) {
3617 var $player = $(evt.target.a);
3618 var playerId = $player.attr('id');
3619 youtubePlayers[playerId] = evt.target; // update stored player
3620 var player = youtubePlayers[playerId];
3621
3622 setParentAsLoading($player);
3623
3624 if (videos[playerId].style === 'muted') {
3625 youtubePlayers[playerId].mute().playVideo().pauseVideo();
3626 } else {
3627 setParentAsLoaded($player);
3628 }
3629
3630 // If first slide or only photo, start video
3631 if ($player.closest(selectors.currentSlide).length || $player.data('image-count') === 1) {
3632 if (videos[playerId].style === 'muted') {
3633 youtubePlayers[playerId].playVideo();
3634 initCheckVisibility(playerId);
3635 }
3636 }
3637 }
3638
3639 function initCheckVisibility(playerId) {
3640 if (!playerId) {
3641 return;
3642 }
3643
3644 // Add out of view pausing
3645 videoVisibilityCheck(playerId);
3646 $(window).on('scroll.' + playerId, {id: playerId}, $.throttle(150, videoVisibilityCheck));
3647 }
3648
3649 function videoVisibilityCheck(id) {
3650 var playerId;
3651
3652 if (!id) {
3653 return;
3654 }
3655
3656 if (typeof id === 'string') {
3657 playerId = id;
3658 } else {
3659 // Data comes in as part of the scroll event
3660 playerId = id.data.id;
3661 }
3662
3663 if (theme.isElementVisible($('#' + playerId))) {
3664 if (videos[playerId] && videos[playerId].style === 'unmuted') {
3665 return;
3666 }
3667 playVisibleVideo(playerId);
3668 } else {
3669 pauseHiddenVideo(playerId);
3670 }
3671 }
3672
3673 function playVisibleVideo(id) {
3674 if (youtubePlayers[id] && typeof youtubePlayers[id].playVideo === 'function') {
3675 youtubePlayers[id].playVideo();
3676 }
3677 }
3678
3679 function pauseHiddenVideo(id) {
3680 if (youtubePlayers[id] && typeof youtubePlayers[id].pauseVideo === 'function') {
3681 youtubePlayers[id].pauseVideo();
3682 }
3683 }
3684
3685 function onVideoStateChange(evt) {
3686 var $player = $(evt.target.a);
3687 var playerId = $player.attr('id');
3688 var player = youtubePlayers[playerId];
3689
3690 switch (evt.data) {
3691 case -1: // unstarted
3692 // Handle low power state on iOS by checking if
3693 // video is reset to unplayed after attempting to buffer
3694 if (videos[playerId].attemptedToPlay) {
3695 setParentAsLoaded($player);
3696 setVideoToBeInteractedWith($player);
3697 }
3698 break;
3699 case 0: // ended
3700 player.playVideo();
3701 break;
3702 case 1: // playing
3703 setParentAsLoaded($player);
3704 break;
3705 case 3: // buffering
3706 videos[playerId].attemptedToPlay = true;
3707 break;
3708 }
3709 }
3710
3711 function setParentAsLoading($el) {
3712 $el
3713 .closest(selectors.videoParent)
3714 .addClass(classes.loading);
3715 }
3716
3717 function setParentAsLoaded($el) {
3718 $el
3719 .closest(selectors.videoParent)
3720 .removeClass(classes.loading)
3721 .addClass(classes.loaded);
3722 }
3723
3724 function setVideoToBeInteractedWith($el) {
3725 $el
3726 .closest(selectors.videoParent)
3727 .addClass(classes.interactable);
3728 }
3729
3730 function Product(container) {
3731 var $container = this.$container = $(container);
3732 var sectionId = this.sectionId = $container.attr('data-section-id');
3733
3734 this.inModal = $container.closest('.screen-layer').length;
3735 this.$modal;
3736
3737 this.namespace = '.product-' + sectionId;
3738 this.namespaceImages = '.product-image-' + sectionId;
3739
3740 this.settings = {
3741 enableHistoryState: $container.data('enable-history-state') || false,
3742 namespace: '.product-' + sectionId,
3743 variantType: $container.data('variant-type'),
3744 inventory: $container.data('inventory') || false,
3745 inventoryThreshold: $container.data('inventory-threshold') || false,
3746 incomingInventory: $container.data('incoming-inventory') || false,
3747 modalInit: false,
3748 slickMainInitialized: false,
3749 slickThumbInitialized: false,
3750 hasImages: true,
3751 hasVideos: $container.find(selectors.productVideo).length || false,
3752 hasMultipleImages: false,
3753 stackedImages: $container.data('images-stacked') || false,
3754 stackedCurrent: 0,
3755 stackedImagePositions: [],
3756 imagesAnimating: false,
3757 imageSize: '620x'
3758 };
3759
3760 // Overwrite some settings when loaded in modal
3761 if (this.inModal) {
3762 this.settings.enableHistoryState = false;
3763 this.namespace = '.product-' + sectionId + '-modal';
3764 this.$modal = $('#ProductScreen-' + sectionId);
3765 }
3766
3767 this.init();
3768 }
3769
3770 Product.prototype = $.extend({}, Product.prototype, {
3771 init: function() {
3772 this.$mainSlider = $(selectors.mainSlider, this.$container);
3773 this.$thumbSlider = $(selectors.thumbSlider, this.$container);
3774 this.$firstProductImage = this.$mainSlider.find('img').first();
3775 this.$formHolder = $(selectors.modalFormHolder + this.sectionId);
3776
3777 if (!this.$firstProductImage.length) {
3778 this.settings.hasImages = false;
3779 }
3780
3781 if (this.inModal) {
3782 this.$container.addClass(classes.isModal);
3783 $('body')
3784 .off('productModalOpen.ProductScreen-' + this.sectionId)
3785 .off('productModalClose.ProductScreen-' + this.sectionId);
3786 $('body').on('productModalOpen.ProductScreen-' + this.sectionId, this.openModalProduct.bind(this));
3787 $('body').on('productModalClose.ProductScreen-' + this.sectionId, this.closeModalProduct.bind(this));
3788 }
3789
3790 if (!this.inModal) {
3791 this.stringOverrides();
3792 this.formSetup();
3793 this.preImageSetup();
3794
3795 this.checkIfVideos();
3796 this.imageSetup(true);
3797 }
3798 },
3799
3800 formSetup: function() {
3801 // Determine how to handle variant availability selectors
3802 if (theme.settings.dynamicVariantsEnable) {
3803 this.$variantSelectors = $(selectors.formContainer, this.$container).find(selectors.singleOptionSelector);
3804 }
3805
3806 this.initAjaxProductForm();
3807 this.initVariants();
3808 },
3809
3810 preImageSetup: function() {
3811 this.setImageSizes();
3812 this.initImageSwitch();
3813 this.initImageZoom();
3814 },
3815
3816 imageSetup: function(needStylesheet) {
3817 if (!this.$thumbSlider.length || $(selectors.photoThumbs, this.$container).length < 2) {
3818 // Single product image. Init video if it exists
3819 var $video = $(selectors.productImageMain, this.$container).find(selectors.productVideo);
3820 if ($video.length) {
3821 this.initVideo($video);
3822 }
3823
3824 return;
3825 }
3826
3827 this.settings.hasMultipleImages = true;
3828
3829 if (needStylesheet) {
3830 slate.utils.promiseStylesheet().then(function() {
3831 this.createImageCarousels();
3832 }.bind(this));
3833 } else {
3834 this.createImageCarousels();
3835 }
3836 },
3837
3838 initImageZoom: function() {
3839 var $container = $(selectors.imageContainer, this.$container);
3840 var imageZoom = new theme.Photoswipe($container, this.sectionId);
3841 },
3842
3843 stringOverrides: function() {
3844 theme.productStrings = theme.productStrings || {};
3845 $.extend(theme.strings, theme.productStrings);
3846 },
3847
3848 setImageSizes: function() {
3849 if (!this.settings.hasImages) {
3850 return;
3851 }
3852
3853 // Get srcset image src, works on most modern browsers
3854 // otherwise defaults to settings.imageSize
3855 var currentImage = this.$firstProductImage[0].currentSrc;
3856
3857 if (currentImage) {
3858 this.settings.imageSize = theme.Images.imageSize(currentImage);
3859 }
3860 },
3861
3862 initVariants: function() {
3863 var $variantJson = $(selectors.variantsJson, this.$container);
3864 if (!$variantJson.length) {
3865 return;
3866 }
3867
3868 this.variantsObject = JSON.parse($variantJson[0].innerHTML);
3869
3870 var options = {
3871 $container: this.$container,
3872 enableHistoryState: this.settings.enableHistoryState,
3873 singleOptionSelector: selectors.singleOptionSelector,
3874 originalSelectorId: selectors.originalSelectorId,
3875 variants: this.variantsObject
3876 };
3877
3878 if ($(selectors.variantColorSwatch, this.$container).length) {
3879 $(selectors.variantColorSwatch, this.$container).on('change', function(evt) {
3880 var $el = $(evt.currentTarget);
3881 var color = $el.data('color-name');
3882 var index = $el.data('color-index');
3883 this.updateColorName(color, index);
3884 }.bind(this));
3885 }
3886
3887 this.variants = new slate.Variants(options);
3888
3889 this.$container
3890 .on('variantChange' + this.namespace, this.updateCartButton.bind(this))
3891 .on('variantImageChange' + this.namespace, this.updateVariantImage.bind(this))
3892 .on('variantPriceChange' + this.namespace, this.updatePrice.bind(this))
3893 .on('variantUnitPriceChange' + this.namespace, this.updateUnitPrice.bind(this));
3894
3895 if ($(selectors.sku, this.$container).length) {
3896 this.$container.on('variantSKUChange' + this.namespace, this.updateSku.bind(this));
3897 }
3898 if (this.settings.inventory || this.settings.incomingInventory) {
3899 this.$container.on('variantChange' + this.namespace, this.updateInventory.bind(this));
3900 }
3901
3902 // Update individual variant availability on each selection
3903 var $currentVariantJson = $(selectors.currentVariantJson, this.$container);
3904
3905 if (theme.settings.dynamicVariantsEnable && $currentVariantJson.length) {
3906 this.currentVariantObject = JSON.parse($currentVariantJson[0].innerHTML);
3907
3908 this.$variantSelectors.on('change' + this.namespace, this.updateVariantAvailability.bind(this));
3909
3910 // Set default state based on current selected variant
3911 this.setCurrentVariantAvailability(this.currentVariantObject, true);
3912 }
3913 },
3914
3915 // Variant change functions
3916 updateColorName: function(color, index) {
3917 // Updates on radio button change, not variant.js
3918 $('#VariantColorLabel-' + this.sectionId + '-' + index).text(color);
3919 },
3920
3921 updateCartButton: function(evt) {
3922 var variant = evt.variant;
3923
3924 if (variant) {
3925 if (variant.available) {
3926 // Available, enable the submit button and change text
3927 $(selectors.addToCart, this.$container).removeClass(classes.disabled).prop('disabled', false);
3928 $(selectors.addToCartText, this.$container).html(theme.strings.addToCart);
3929 } else {
3930 // Sold out, disable the submit button and change text
3931 $(selectors.addToCart, this.$container).addClass(classes.disabled).prop('disabled', true);
3932 $(selectors.addToCartText, this.$container).html(theme.strings.soldOut);
3933 }
3934 } else {
3935 // The variant doesn't exist, disable submit button
3936 $(selectors.addToCart, this.$container).addClass(classes.disabled).prop('disabled', true);
3937 $(selectors.addToCartText, this.$container).html(theme.strings.unavailable);
3938 }
3939 },
3940
3941 updatePrice: function(evt) {
3942 var variant = evt.variant;
3943
3944 if (variant) {
3945 // Regular price
3946 $(selectors.price, this.$container).html(theme.Currency.formatMoney(variant.price, theme.settings.moneyFormat)).show();
3947
3948 // Sale price, if necessary
3949 if (variant.compare_at_price > variant.price) {
3950 $(selectors.comparePrice, this.$container).html(theme.Currency.formatMoney(variant.compare_at_price, theme.settings.moneyFormat));
3951 $(selectors.priceWrapper, this.$container).removeClass('hide');
3952 $(selectors.price, this.$container).addClass(classes.onSale);
3953 $(selectors.comparePriceA11y, this.$container).attr('aria-hidden', 'false');
3954 $(selectors.priceA11y, this.$container).attr('aria-hidden', 'false');
3955 } else {
3956 $(selectors.priceWrapper, this.$container).addClass('hide');
3957 $(selectors.price, this.$container).removeClass(classes.onSale);
3958 $(selectors.comparePriceA11y, this.$container).attr('aria-hidden', 'true');
3959 $(selectors.priceA11y, this.$container).attr('aria-hidden', 'true');
3960 }
3961
3962 if (theme.settings.currenciesEnabled) {
3963 theme.currencySwitcher.ajaxrefresh();
3964 }
3965 }
3966 },
3967
3968 updateUnitPrice: function(evt) {
3969 var variant = evt.variant;
3970
3971 if (variant && variant.unit_price) {
3972 var price = theme.Currency.formatMoney(variant.unit_price, theme.settings.moneyFormat);
3973 var base = theme.Currency.getBaseUnit(variant);
3974
3975 $(selectors.unitWrapper, this.$container)
3976 .html(price + '/' + base)
3977 .removeClass('hide').removeClass(classes.visuallyHide);
3978 } else {
3979 $(selectors.unitWrapper, this.$container).addClass(classes.visuallyHide);
3980 }
3981 },
3982
3983 updateSku: function(evt) {
3984 var variant = evt.variant;
3985 var newSku = '';
3986
3987 if (variant) {
3988 if (variant.sku) {
3989 newSku = variant.sku;
3990 }
3991
3992 $(selectors.sku, this.$container).html(newSku);
3993 }
3994 },
3995
3996 updateInventory: function(evt) {
3997 var variant = evt.variant;
3998
3999 // If we don't track variant inventory, hide stock
4000 if (!variant || !variant.inventory_management) {
4001 this.toggleInventoryQuantity(false);
4002 this.toggleIncomingInventory(false);
4003 return;
4004 }
4005
4006 if (variant.inventory_management === 'shopify' && window.inventories && window.inventories[this.sectionId]) {
4007 variantInventoryObject = window.inventories[this.sectionId][variant.id];
4008 var quantity = variantInventoryObject.quantity;
4009 var showInventory = true;
4010 var showIncomingInventory = false;
4011
4012 if (quantity <= 0 || quantity > theme.settings.inventoryThreshold) {
4013 showInventory = false;
4014 }
4015
4016 this.toggleInventoryQuantity(showInventory, quantity);
4017
4018 if (!showInventory && variantInventoryObject.incoming) {
4019 showIncomingInventory = true;
4020 }
4021
4022 this.toggleIncomingInventory(showIncomingInventory, variant.available, variantInventoryObject.next_incoming_date);
4023 }
4024 },
4025
4026 toggleInventoryQuantity: function(show, qty) {
4027 if (!this.settings.inventory) {
4028 show = false;
4029 }
4030
4031 if (show) {
4032 $(selectors.inventory, this.$container)
4033 .removeClass('hide')
4034 .text(theme.strings.stockLabel.replace('[count]', qty));
4035 } else {
4036 $(selectors.inventory, this.$container).addClass('hide');
4037 }
4038 },
4039
4040 toggleIncomingInventory: function(show, available, date) {
4041 if (!this.settings.incomingInventory) {
4042 show = false;
4043 }
4044
4045 if (show) {
4046 var string = available ?
4047 theme.strings.willNotShipUntil.replace('[date]', date) :
4048 theme.strings.willBeInStockAfter.replace('[date]', date);
4049
4050 if (!date) {
4051 string = theme.strings.waitingForStock;
4052 }
4053
4054 $(selectors.incomingInventory, this.$container)
4055 .removeClass('hide')
4056 .text(string);
4057 } else {
4058 $(selectors.incomingInventory, this.$container).addClass('hide');
4059 }
4060 },
4061
4062 updateVariantImage: function(evt) {
4063 var variant = evt.variant;
4064 var sizedImgUrl = theme.Images.getSizedImageUrl(variant.featured_image.src, this.settings.imageSize);
4065
4066 var $newImage = $('.product__thumb[data-id="' + variant.featured_image.id + '"]');
4067 var imageIndex = this._slideIndex($newImage.closest('.product__thumb-item'));
4068
4069 // No image, bail
4070 if (typeof imageIndex === 'undefined') {
4071 return;
4072 }
4073
4074 if (!theme.config.bpSmall && this.settings.stackedImages) {
4075 this.stackedScrollTo(imageIndex);
4076 } else {
4077 this.$mainSlider.slick('slickGoTo', imageIndex);
4078 }
4079 },
4080
4081 setCurrentVariantAvailability: function(variant) {
4082 var valuesToEnable = {
4083 option1: [],
4084 option2: [],
4085 option3: []
4086 };
4087
4088 // Disable all options to start
4089 this.disableVariantGroup($(selectors.formContainer, this.$container).find('.variant-input-wrap'));
4090
4091 // Combine all available variants
4092 var availableVariants = this.variantsObject.filter(function(el) {
4093 if (variant.id === el.id) {
4094 return false;
4095 }
4096
4097 // Option 1
4098 if (variant.option2 === el.option2 && variant.option3 === el.option3) {
4099 return true;
4100 }
4101
4102 // Option 2
4103 if (variant.option1 === el.option1 && variant.option3 === el.option3) {
4104 return true;
4105 }
4106
4107 // Option 3
4108 if (variant.option1 === el.option1 && variant.option2 === el.option2) {
4109 return true;
4110 }
4111 });
4112
4113
4114 // IE11 can't handle shortform of {variant} so extra step is needed
4115 var variantObject = {
4116 variant: variant
4117 };
4118
4119 availableVariants = Object.assign({}, variantObject, availableVariants);
4120
4121 // Loop through each available variant to gather variant values
4122 for (var property in availableVariants) {
4123 if (availableVariants.hasOwnProperty(property)) {
4124 var item = availableVariants[property];
4125 var option1 = item.option1;
4126 var option2 = item.option2;
4127 var option3 = item.option3;
4128
4129 if (option1) {
4130 if (valuesToEnable.option1.indexOf(option1) === -1) {
4131 valuesToEnable.option1.push(option1);
4132 }
4133 }
4134 if (option2) {
4135 if (valuesToEnable.option2.indexOf(option2) === -1) {
4136 valuesToEnable.option2.push(option2);
4137 }
4138 }
4139 if (option3) {
4140 if (valuesToEnable.option3.indexOf(option3) === -1) {
4141 valuesToEnable.option3.push(option3);
4142 }
4143 }
4144 }
4145 }
4146
4147 // Have values to enable, separated by option index
4148 if (valuesToEnable.option1.length) {
4149 this.enableVariantOptionByValue(valuesToEnable.option1, 'option1');
4150 }
4151 if (valuesToEnable.option2.length) {
4152 this.enableVariantOptionByValue(valuesToEnable.option2, 'option2');
4153 }
4154 if (valuesToEnable.option3.length) {
4155 this.enableVariantOptionByValue(valuesToEnable.option3, 'option3');
4156 }
4157 },
4158
4159 updateVariantAvailability: function(evt, value, index) {
4160 if (value && index) {
4161 var newVal = value;
4162 var optionIndex = index;
4163 } else {
4164 var $el = $(evt.currentTarget);
4165 var newVal = $el.val() ? $el.val() : evt.currentTarget.value;
4166 var optionIndex = $el.data('index');
4167 }
4168
4169 var variants = this.variantsObject.filter(function(el) {
4170 return el[optionIndex] === newVal;
4171 });
4172
4173 // Disable all buttons/dropdown options that aren't the current index
4174 $(selectors.formContainer, this.$container).find('.variant-input-wrap').each(function(index, el) {
4175 var $group = $(el);
4176 var currentOptionIndex = $group.data('index');
4177
4178 if (currentOptionIndex !== optionIndex) {
4179 // Disable all options as a starting point
4180 this.disableVariantGroup($group);
4181
4182 // Loop through legit available options and enable
4183 for (var i = 0; i < variants.length; i++) {
4184 this.enableVariantOption($group, variants[i][currentOptionIndex]);
4185 }
4186 }
4187 }.bind(this));
4188 },
4189
4190 disableVariantGroup: function($group) {
4191 if (this.settings.variantType === 'dropdown') {
4192 $group.find('option').prop('disabled', true)
4193 } else {
4194 $group.find('input').prop('disabled', true);
4195 $group.find('label').toggleClass('disabled', true);
4196 }
4197 },
4198
4199 enableVariantOptionByValue: function(array, index) {
4200 var $group = $(selectors.formContainer, this.$container).find('.variant-input-wrap[data-index="'+ index +'"]');
4201
4202 for (var i = 0; i < array.length; i++) {
4203 this.enableVariantOption($group, array[i]);
4204 }
4205 },
4206
4207 enableVariantOption: function($group, value) {
4208 // Selecting by value so escape it
4209 value = value.replace(/([ #;&,.+*~\':"!^$[\]()=>|\/@])/g,'\\$1');
4210
4211 if (this.settings.variantType === 'dropdown') {
4212 $group.find('option[value="'+ value +'"]').prop('disabled', false);
4213 } else {
4214 var $buttonGroup = $group.find('.variant-input[data-value="'+ value +'"]');
4215 $buttonGroup.find('input').prop('disabled', false);
4216 $buttonGroup.find('label').toggleClass('disabled', false);
4217 }
4218 },
4219
4220 // Image/thumbnail toggling
4221 initImageSwitch: function() {
4222 if (!$(selectors.photoThumbs, this.$container).length) {
4223 return;
4224 }
4225
4226 var self = this;
4227
4228 $(selectors.photoThumbs, this.$container).on('click', function(evt) {
4229 evt.preventDefault();
4230 if (!theme.config.bpSmall && this.settings.stackedImages) {
4231 var index = $(evt.currentTarget).data('index');
4232 this.stackedScrollTo(index);
4233 }
4234 }.bind(this));
4235 },
4236
4237 checkIfVideos: function() {
4238 var $productVideos = this.$mainSlider.find(selectors.productVideo);
4239
4240 // Stop if there are 0 videos
4241 if (!$productVideos.length) {
4242 return false;
4243 }
4244
4245 var videoTypes = [];
4246
4247 $productVideos.each(function() {
4248 var type = $(this).data('video-type');
4249
4250 if (videoTypes.indexOf(type) < 0) {
4251 videoTypes.push(type);
4252 }
4253 });
4254
4255 // Load YouTube API if not already loaded
4256 if (videoTypes.indexOf('youtube') > -1) {
4257 if (!theme.config.youTubeReady) {
4258 window.loadYouTube();
4259 $('body').on('youTubeReady' + this.namespace, function() {
4260 this.loadYoutubeVideos($productVideos);
4261 }.bind(this));
4262 } else {
4263 this.loadYoutubeVideos($productVideos);
4264 }
4265 }
4266
4267 // Load Vimeo API if not already loaded
4268 if (videoTypes.indexOf('vimeo') > -1) {
4269 if (!vimeoReady) {
4270 window.loadVimeo();
4271 $('body').on('vimeoReady' + this.namespace, function() {
4272 this.loadVimeoVideos($productVideos);
4273 }.bind(this))
4274 } else {
4275 this.loadVimeoVideos($productVideos);
4276 }
4277 }
4278
4279 // Add mp4 video players
4280 if (videoTypes.indexOf('mp4') > -1) {
4281 this.loadMp4Videos($productVideos);
4282 }
4283
4284 return videoTypes;
4285 },
4286
4287 loadMp4Videos: function($videos) {
4288 $videos.each(function() {
4289 var $el = $(this);
4290 if ($el.data('video-type') != 'mp4') {
4291 return;
4292 }
4293
4294 var id = $el.attr('id');
4295 var videoId = $el.data('video-id');
4296
4297 videos[this.id] = {
4298 type: 'mp4',
4299 divId: id,
4300 style: $el.data('video-style')
4301 };
4302 });
4303 },
4304
4305 loadVimeoVideos: function($videos) {
4306 $videos.each(function() {
4307 var $el = $(this);
4308 if ($el.data('video-type') != 'vimeo') {
4309 return;
4310 }
4311
4312 var id = $el.attr('id');
4313 var videoId = $el.data('video-id');
4314
4315 videos[this.id] = {
4316 type: 'vimeo',
4317 divId: id,
4318 id: videoId,
4319 style: $el.data('video-style'),
4320 width: $el.data('video-width'),
4321 height: $el.data('video-height')
4322 };
4323 });
4324
4325 // Create a new player for each Vimeo video
4326 for (var key in videos) {
4327 if (videos[key].type != 'vimeo') {
4328 continue;
4329 }
4330
4331 var args = $.extend({}, vimeoVideoOptions, videos[key]);
4332 vimeoPlayers[key] = new Vimeo.Player(videos[key].divId, args);
4333 }
4334
4335 vimeoReady = true;
4336 },
4337
4338 autoplayVimeoVideo: function(id) {
4339 // Do not autoplay on mobile though
4340 if (!theme.config.bpSmall) {
4341 this.requestToPlayVimeoVideo(id);
4342 } else {
4343 // Set as loaded on mobile so you can see the image
4344 var $player = $('#' + id);
4345 setParentAsLoaded($player);
4346 }
4347 },
4348
4349 requestToPlayVimeoVideo: function(id) {
4350 // The slider may initialize and attempt to play the video before
4351 // the API is even ready, because it sucks.
4352
4353 var $player = $('#' + id);
4354 setParentAsLoading($player);
4355
4356 if (!vimeoReady) {
4357 // Wait for the trigger, then play it
4358 $('body').on('vimeoReady' + this.namespace, function() {
4359 this.playVimeoVideo(id);
4360 }.bind(this))
4361 return;
4362 }
4363
4364 this.playVimeoVideo(id);
4365 },
4366
4367 playVimeoVideo: function(id) {
4368 vimeoPlayers[id].play();
4369
4370 if (videos[id].style === 'muted') {
4371 vimeoPlayers[id].setVolume(0);
4372 }
4373
4374 var $player = $('#' + id);
4375 setParentAsLoaded($player);
4376 },
4377
4378 stopVimeoVideo: function(id) {
4379 if (!theme.config.vimeoReady) {
4380 return;
4381 }
4382
4383 if (id) {
4384 vimeoPlayers[id].pause();
4385 } else {
4386 for (key in vimeoPlayers) {
4387 if (typeof vimeoPlayers[key].pause === 'function') {
4388 vimeoPlayers[key].pause();
4389 }
4390 }
4391 }
4392 },
4393
4394 loadYoutubeVideos: function($videos) {
4395 $videos.each(function() {
4396 var $el = $(this);
4397 if ($el.data('video-type') != 'youtube') {
4398 return;
4399 }
4400
4401 var id = $el.attr('id');
4402 var videoId = $el.data('youtube-id');
4403
4404 videos[this.id] = {
4405 type: 'youtube',
4406 id: id,
4407 videoId: videoId,
4408 style: $el.data('video-style'),
4409 width: $el.data('video-width'),
4410 height: $el.data('video-height'),
4411 attemptedToPlay: false
4412 };
4413 });
4414
4415 // Create a player for each YouTube video
4416 for (var key in videos) {
4417 if (videos[key].type === 'youtube') {
4418 if (videos.hasOwnProperty(key)) {
4419 var args = $.extend({}, youtubeVideoOptions, videos[key]);
4420
4421 if (args.style === 'muted') {
4422 // default youtubeVideoOptions, no need to change anything
4423 } else {
4424 args.playerVars.controls = 1;
4425 args.playerVars.autoplay = 0;
4426 }
4427
4428 youtubePlayers[key] = new YT.Player(key, args);
4429 }
4430 }
4431 }
4432
4433 youtubeReady = true;
4434 },
4435
4436 requestToPlayYoutubeVideo: function(id, forcePlay) {
4437 if (!theme.config.youTubeReady) {
4438 return;
4439 }
4440
4441 var $player = $('#' + id);
4442 setParentAsLoading($player);
4443
4444 // If video is requested too soon, player might not be ready.
4445 // Set arbitrary timeout to request it again in a second
4446 if (typeof youtubePlayers[id].playVideo != 'function') {
4447 var o = this;
4448 setTimeout(function() {
4449 o.playYoutubeVideo(id, forcePlay);
4450 }, 1000);
4451 return;
4452 }
4453
4454 this.playYoutubeVideo(id, forcePlay);
4455 },
4456
4457 playYoutubeVideo: function (id, forcePlay) {
4458 var $player = $('#' + id);
4459 setParentAsLoaded($player);
4460 if (typeof youtubePlayers[id].playVideo === 'function') {
4461 youtubePlayers[id].playVideo();
4462 }
4463
4464 // forcePlay is sent as true from beforeSlideChange so the visibility
4465 // check isn't fooled by the next slide positioning
4466 if (!forcePlay) {
4467 initCheckVisibility(id);
4468 }
4469 },
4470
4471 stopYoutubeVideo: function(id) {
4472 if (!theme.config.youTubeReady) {
4473 return;
4474 }
4475
4476 if (id && youtubePlayers[id]) {
4477 if (typeof youtubePlayers[id].pauseVideo === 'function') {
4478 youtubePlayers[id].pauseVideo();
4479 }
4480 $(window).off('scroll.' + id);
4481 } else {
4482 for (key in youtubePlayers) {
4483 if (typeof youtubePlayers[key].pauseVideo === 'function') {
4484 youtubePlayers[key].pauseVideo();
4485 $(window).off('scroll.' + key);
4486 }
4487 }
4488 }
4489 },
4490
4491 playMp4Video: function(id) {
4492 var $player = $('#' + id);
4493 setParentAsLoaded($player);
4494
4495 $player[0].play();
4496 },
4497
4498 stopMp4Video: function(id) {
4499 if (id) {
4500 $('#' + id)[0].pause();
4501 } else {
4502 // loop through all mp4 videos to stop them
4503 for (var key in videos) {
4504 if (videos[key].type === 'mp4') {
4505 var player = $('#' + videos[key].divId)[0];
4506 if (typeof player.pause === 'function') {
4507 player.pause();
4508 }
4509 }
4510 }
4511 }
4512 },
4513
4514 stackedImagesInit: function() {
4515 $(window).off(this.namespaceImages);
4516 this.stackedImagePositions();
4517
4518 if (this.inModal) {
4519 // Slight delay in modal to accommodate loading videos
4520 setTimeout(function() {
4521 this.stackedActive(this.settings.stackedCurrent);
4522 }.bind(this), 1000);
4523 } else {
4524 this.stackedActive(this.settings.stackedCurrent);
4525 }
4526
4527
4528 // update image positions on resize
4529 $(window).on('resize' + this.namespaceImages, $.debounce(200, this.stackedImagePositions.bind(this)));
4530
4531 // scroll listener to mark active thumbnail
4532 $(window).on('scroll' + this.namespaceImages, $.throttle(200, function() {
4533 var goal = window.scrollY;
4534 var closest = this.settings.stackedImagePositions.reduce(function(prev, curr) {
4535 return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev);
4536 });
4537 var index = this.settings.stackedImagePositions.indexOf(closest)
4538 if (this.settings.stackedCurrent !== index) {
4539 this.stackedActive(index);
4540 }
4541 }.bind(this)));
4542 },
4543
4544 stackedImagePositions: function() {
4545 var positions = [];
4546 $(selectors.photo, this.$container).each(function() {
4547 positions.push(Math.round($(this).offset().top));
4548 });
4549 this.settings.stackedImagePositions = positions;
4550 },
4551
4552 stackedScrollTo: function(index) {
4553 // Scroll to top of large image
4554 var pos = $(selectors.photo, this.$container).eq(index).offset().top;
4555 $('html, body').animate({
4556 scrollTop: pos
4557 }, 400, 'swing');
4558 },
4559
4560 stackedActive: function(index) {
4561 $(selectors.photoThumbItem, this.$container)
4562 .removeClass(classes.thumbActive)
4563 .eq(index).addClass(classes.thumbActive);
4564
4565 if (this.settings.hasVideos) {
4566 this.stopVideo();
4567
4568 var $video = $(selectors.photo, this.$container).eq(index).find(selectors.productVideo);
4569
4570 if ($video.length) {
4571 this.initVideo($video);
4572 }
4573 }
4574
4575 this.settings.stackedCurrent = index;
4576 },
4577
4578 createImageCarousels: function() {
4579 // Set starting slide (for both sliders)
4580 var $activeSlide = this.$mainSlider.find('.starting-slide');
4581 var startIndex = this._slideIndex($activeSlide);
4582
4583 // Lame way to prevent duplicate event listeners
4584 this.$mainSlider.off('init');
4585 this.$mainSlider.off('beforeChange');
4586 this.$mainSlider.on('init', this.mainSlideInit.bind(this));
4587 this.$mainSlider.on('beforeChange', this.beforeSlideChange.bind(this));
4588 this.$thumbSlider.on('init', this.thumbSlideInit.bind(this));
4589
4590 // Default (mobile) slider settings
4591 this.mainSliderArgs = {
4592 infinite: true,
4593 arrows: false,
4594 dots: true,
4595 touchThreshold: 10,
4596 speed: 300,
4597 adaptiveHeight: true,
4598 initialSlide: startIndex
4599 };
4600
4601 this.thumbSliderArgs = {
4602 initialSlide: startIndex
4603 };
4604
4605 // Init sliders normally
4606 var sliderArgs = this.setSliderArgs();
4607 this.initSliders(sliderArgs);
4608
4609 // Re-init slider when a breakpoint is hit
4610 $('body').on('matchSmall matchLarge', function() {
4611 var sliderArgs = this.setSliderArgs();
4612 this.initSliders(sliderArgs);
4613 }.bind(this));
4614
4615 // Too many thumbnails can cause the AOS calculations to be off
4616 // so refresh that when the slider is ready
4617 if (AOS) {
4618 AOS.refresh();
4619 }
4620 },
4621
4622 initSliders: function(args) {
4623 this.destroyImageCarousels();
4624
4625 if (!theme.config.bpSmall && this.settings.stackedImages) {
4626 this.stackedImagesInit();
4627 } else {
4628 this.$mainSlider.not('.slick-initialized').slick(args.main);
4629 }
4630
4631 if (!theme.config.bpSmall && !this.settings.stackedImages) {
4632 if (this.$thumbSlider.length) {
4633 this.$thumbSlider.not('.slick-initialized').slick(args.thumbs);
4634 }
4635 }
4636 },
4637
4638 setSliderArgs: function() {
4639 var args = {};
4640 var thumbnailsVertical = this.$thumbSlider.data('position') === 'beside' ? true : false;
4641
4642 if (theme.config.bpSmall) {
4643 args.main = this.mainSliderArgs;
4644 args.thumbs = this.thumbSliderArgs;
4645 } else {
4646 args.main = $.extend({}, this.mainSliderArgs, {
4647 asNavFor: '#' + this.$thumbSlider.attr('id'),
4648 adaptiveHeight: thumbnailsVertical ? false : true,
4649 dots: false,
4650 infinite: false,
4651 fade: true
4652 });
4653
4654 args.thumbs = $.extend({}, this.thumbSliderArgs, {
4655 asNavFor: '#' + this.$mainSlider.attr('id'),
4656 slidesToShow: thumbnailsVertical ? 3 : 5,
4657 slidesToScroll: 1,
4658 arrows: false,
4659 dots: false,
4660 vertical: thumbnailsVertical,
4661 verticalSwiping: thumbnailsVertical,
4662 focusOnSelect: true,
4663 infinite: false,
4664 customHeightMatching: thumbnailsVertical,
4665 customSlideAdvancement: true
4666 });
4667 }
4668
4669 return args;
4670 },
4671
4672 destroyImageCarousels: function() {
4673 if (this.$mainSlider && this.settings.slickMainInitialized) {
4674 this.$mainSlider.slick('unslick');
4675 this.settings.slickMainInitialized = false;
4676 }
4677
4678 if (this.$thumbSlider && this.settings.slickThumbInitialized) {
4679 this.$thumbSlider.slick('unslick');
4680 this.settings.slickThumbInitialized = false;
4681 }
4682
4683 this.settings.slickMainInitialized = false;
4684 this.settings.slickThumbInitialized = false;
4685 },
4686
4687 mainSlideInit: function(event, slick) {
4688 var $currentSlide = slick.$slider.find(selectors.currentSlide);
4689 var $video = $currentSlide.find(selectors.productVideo);
4690
4691 this.settings.slickMainInitialized = true;
4692
4693 if (!$video.length) {
4694 return;
4695 }
4696
4697 this.initVideo($video);
4698 },
4699
4700 thumbSlideInit: function(event, slick) {
4701 this.settings.slickThumbInitialized = true;
4702 },
4703
4704 initVideo: function($video) {
4705 var videoType = $video.data('video-type');
4706 var divId = $video.attr('id');
4707
4708 if (videoType === 'mp4' && videos[divId].style === 'muted') {
4709 this.playMp4Video(divId);
4710 }
4711
4712 if (videoType === 'youtube') {
4713 if (youtubeReady && videos[divId].style === 'muted') {
4714 this.requestToPlayYoutubeVideo(divId);
4715 }
4716 }
4717
4718 if (videoType === 'vimeo') {
4719 if (vimeoReady) {
4720 this.playOrShowVimeo(divId);
4721 } else {
4722 $('body').on('vimeoReady' + this.namespace, function() {
4723 this.playOrShowVimeo(divId);
4724 }.bind(this))
4725 }
4726 }
4727
4728 // Hacky way to trigger resetting the slider layout in modals
4729 if (this.inModal) {
4730 this.resizeSlides();
4731 }
4732 },
4733
4734 stopVideo: function(id, type) {
4735 if (!id) {
4736 this.stopYoutubeVideo();
4737 this.stopVimeoVideo();
4738 this.stopMp4Video();
4739 }
4740
4741 if (type === 'youtube') {
4742 this.stopYoutubeVideo(id);
4743 }
4744
4745 if (type === 'mp4') {
4746 this.stopMp4Video(id);
4747 }
4748
4749 if (type === 'vimeo') {
4750 this.stopVimeoVideo(id);
4751 }
4752 },
4753
4754 playOrShowVimeo: function(id) {
4755 if (videos[id] && videos[id].style === 'muted') {
4756 this.autoplayVimeoVideo(id);
4757 } else if (videos[id] && videos[id].style === 'unmuted') {
4758 setParentAsLoaded($('#' + id));
4759 }
4760 },
4761
4762 getVideoType: function($video) {
4763 return $video.data('video-type');
4764 },
4765
4766 getVideoId: function($video) {
4767 return $video.attr('id');
4768 },
4769
4770 beforeSlideChange: function(event, slick, currentSlide, nextSlide) {
4771 var $slider = slick.$slider;
4772 var $currentSlide = $slider.find(selectors.currentSlide);
4773 var $prevVideo = $currentSlide.find('.product__video');
4774 var $nextSlide = $slider.find('.slick-slide[data-slick-index="' + nextSlide + '"]');
4775 var $nextVideo = $nextSlide.find('.product__video');
4776
4777 // Pause any existing slide video
4778 if (currentSlide !== nextSlide && $prevVideo.length) {
4779 var prevVideoType = this.getVideoType($prevVideo);
4780 var prevVideoId = this.getVideoId($prevVideo);
4781
4782 if (prevVideoId) {
4783 this.stopVideo(prevVideoId, prevVideoType);
4784 }
4785 }
4786
4787 // Prep next slide video
4788 if ($nextVideo.length) {
4789 var nextVideoType = this.getVideoType($nextVideo);
4790 var nextVideoId = this.getVideoId($nextVideo);
4791
4792 // Prep Vimeo with a backup in case the API isn't ready
4793 if (nextVideoId && nextVideoType === 'vimeo') {
4794 if (vimeoReady) {
4795 if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') {
4796 this.autoplayVimeoVideo(nextVideoId);
4797 }
4798 } else {
4799 $('body').on('vimeoReady' + this.namespace, function() {
4800 if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') {
4801 this.autoplayVimeoVideo(nextVideoId);
4802 }
4803 }.bind(this))
4804 }
4805 }
4806
4807 // Prep YouTube with a backup in case API isn't ready
4808 if (nextVideoId && nextVideoType === 'youtube') {
4809 if (youtubeReady) {
4810 if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') {
4811 this.requestToPlayYoutubeVideo(nextVideoId, true);
4812 }
4813 } else {
4814 $('body').on('youTubeReady' + this.namespace, function() {
4815 if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') {
4816 this.requestToPlayYoutubeVideo(nextVideoId, true);
4817 }
4818 }.bind(this))
4819 }
4820 }
4821
4822 // Autoplay muted MP4 videos
4823 if (nextVideoId && videos[nextVideoId] && videos[nextVideoId].style === 'muted') {
4824 if (nextVideoType === 'mp4') {
4825 this.playMp4Video(nextVideoId);
4826 }
4827 }
4828
4829 // Set unmuted videos to loaded state
4830 if (nextVideoId && videos[nextVideoId] && videos[nextVideoId].style != 'muted') {
4831 setParentAsLoaded($('#' + nextVideoId));
4832 }
4833 }
4834 },
4835
4836 resizeSlides: function() {
4837 if (!this.settings.hasMultipleImages) {
4838 return;
4839 }
4840
4841 // Necessary to make slider visible again
4842 $(window).trigger('resize.slick');
4843 setTimeout(function() {
4844 if (this.$mainSlider && this.settings.slickMainInitialized) {
4845 this.$mainSlider.slick('setPosition');
4846 }
4847 if (this.$thumbSlider && this.settings.slickThumbInitialized) {
4848 this.$thumbSlider.slick('setPosition');
4849 }
4850 }.bind(this), 500); // same timing as modal open transition
4851 },
4852
4853 _slideIndex: function($el) {
4854 return $el.data('index');
4855 },
4856
4857 initAjaxProductForm: function() {
4858 if (theme.settings.cartType === 'drawer' || theme.settings.cartType === 'sticky') {
4859 new theme.AjaxProduct($(selectors.formContainer, this.$container));
4860 }
4861 },
4862
4863 openModalProduct: function() {
4864 var initialized = false;
4865 if (!this.settings.modalInit) {
4866 var url = this.$formHolder.data('url');
4867 var template = this.$formHolder.data('template');
4868
4869 // If not template, product uses default product template
4870 // which has sections. Ajax view is a slimmed down version to
4871 // load only essentials
4872 if (!template) {
4873 url = url + '?view=ajax';
4874 }
4875
4876 $.get(url, function(data) {
4877 var $template = $(data);
4878 var $newForm = $template.find('#AddToCartForm-' + this.sectionId);
4879 this.replaceModalFormHolder(this.$formHolder, $newForm);
4880
4881 var $sectionDiv = $template.find('#ProductSections-' + this.sectionId);
4882 if ($sectionDiv.length) {
4883 this.loadProductSections($sectionDiv);
4884 }
4885
4886 var $relatedDiv = $template.find('#Recommendations-' + this.sectionId);
4887 if ($relatedDiv.length) {
4888 this.loadRelatedProducts($relatedDiv);
4889 }
4890
4891 var $socialDiv = $template.find('.index-section.instagram-section');
4892 if ($socialDiv.length) {
4893 this.loadSocialSection($socialDiv);
4894 }
4895
4896 if (window.SPR) {
4897 SPR.initDomEls();SPR.loadBadges();
4898 }
4899
4900 sections.loadSubSections(this.$modal);
4901
4902 document.dispatchEvent(new CustomEvent('quickview:loaded', {
4903 detail: {
4904 productId: this.sectionId
4905 }
4906 }));
4907 }.bind(this));
4908
4909 this.preImageSetup();
4910 this.loadModalContent();
4911 this.imageSetup(false);
4912 this.settings.modalInit = true;
4913 } else {
4914 initialized = true;
4915 if (!theme.config.bpSmall && this.settings.stackedImages) {
4916 this.stackedActive(0);
4917 }
4918 }
4919
4920 document.dispatchEvent(new CustomEvent('quickview:open', {
4921 detail: {
4922 initialized: initialized,
4923 productId: this.sectionId
4924 }
4925 }));
4926
4927 this.resizeSlides();
4928 },
4929
4930 closeModalProduct: function() {
4931 this.stopVideo();
4932 $('body').off(this.namespace);
4933 $(window).off(this.namespace);
4934 },
4935
4936 replaceModalFormHolder: function($holder, $form) {
4937 $holder.replaceWith($form);
4938 this.formSetup();
4939 if (Shopify.PaymentButton) {
4940 Shopify.PaymentButton.init();
4941 }
4942 },
4943
4944 loadProductSections: function($content) {
4945 $('#ProductSectionsHolder-' + this.sectionId).replaceWith($content);
4946 },
4947
4948 loadRelatedProducts: function($content) {
4949 // Remove any quick view modals as they cause conflicts.
4950 // These are not output with product.ajax templates,
4951 // but are in our custom product.sections ones and any custom ones
4952 // developers create.
4953 $content.find('.screen-layer--product').remove();
4954
4955 $('#ProductRelatedHolder-' + this.sectionId).replaceWith($content);
4956 },
4957
4958 loadSocialSection: function($content) {
4959 $('#SocialSectionHolder-' + this.sectionId).replaceWith($content);
4960 },
4961
4962 loadModalContent: function() {
4963 // Load videos if they exist
4964 var videoTypes = this.checkIfVideos();
4965
4966 // Lazyload mp4 videos similar to images
4967 if (videoTypes && videoTypes.indexOf('mp4') > -1) {
4968 this.$modal.find('.product__video[data-video-type="mp4"]').each(function(i, video) {
4969 var $el = $(video);
4970 var src = $el.data('video-src');
4971 var source = document.createElement('source');
4972 source.setAttribute('src', src);
4973 $el.append(source);
4974 }.bind(this));
4975 }
4976 },
4977
4978 onUnload: function() {
4979 this.$container.off(this.namespace);
4980 $('body').off(this.namespace);
4981 $(window).off(this.namespace).off(this.namespaceImages);;
4982 this.destroyImageCarousels();
4983
4984 if (AOS) {
4985 AOS.refresh();
4986 }
4987 }
4988 });
4989
4990 return Product;
4991 })();
4992
4993 theme.Recommendations = (function() {
4994
4995 function Recommendations(container) {
4996 var $container = this.$container = $(container);
4997 var sectionId = this.sectionId = $container.attr('data-section-id');
4998
4999 this.selectors = {
5000 recommendations: '#Recommendations-' + sectionId,
5001 placeholder: '.product-recommendations-placeholder',
5002 sectionClass: ' .product-recommendations',
5003 productResults: '.grid-product'
5004 };
5005
5006 this.init();
5007 }
5008
5009 Recommendations.prototype = $.extend({}, Recommendations.prototype, {
5010 init: function() {
5011 var $section = $(this.selectors.recommendations);
5012
5013 if (!$section.length || $section.data('enable') === false) {
5014 return;
5015 }
5016
5017 var $placeholder = $section.find(this.selectors.placeholder);
5018 var id = $section.data('product-id');
5019 var limit = $section.data('limit');
5020
5021 var url = '/recommendations/products?section_id=product-recommendations&limit='+ limit +'&product_id=' + id;
5022
5023 $placeholder.load(url + this.selectors.sectionClass, function(data) {
5024 theme.reinitProductGridItem($section);
5025
5026 // If no results, hide the entire section
5027 if ($(data).find(this.selectors.sectionClass).find(this.selectors.productResults).length === 0) {
5028 $section.addClass('hide');
5029 }
5030 }.bind(this));
5031 }
5032 });
5033
5034 return Recommendations;
5035 })();
5036
5037 // Handles multiple section interactions:
5038 // - Featured collection slider
5039 // - Featured collection grid (hover product sliders only)
5040 // - Related products
5041 // - Social reviews
5042 //
5043 // Options:
5044 // - scrollable: overflow div with arrows
5045 // - infinite pagination: only in slider format
5046
5047 theme.FeaturedCollection = (function() {
5048 var selectors = {
5049 scrollWrap: '[data-pagination-wrapper]',
5050 scrollAnimation: '[data-aos="overflow__animation"]',
5051 productContainer: '[data-product-container]',
5052 collectionProductContainer: '[data-collection-container]',
5053 product: '[data-product-grid]',
5054 arrows: '[data-arrow]'
5055 };
5056
5057 var classes = {
5058 loading: 'collection-loading',
5059 arrowLeft: 'overflow-scroller__arrow--left',
5060 disableScrollLeft: 'overflow-scroller--disable-left',
5061 disableScrollRight: 'overflow-scroller--disable-right'
5062 };
5063
5064 var config = {
5065 instagramLoaded: false
5066 };
5067
5068 function FeaturedCollection(container) {
5069 this.$container = $(container);
5070 this.sectionId = this.$container.attr('data-section-id');
5071 this.$scrollWrap = $(selectors.scrollWrap, this.$container);
5072 this.$scrollArrows = $(selectors.arrows, this.$container);
5073 this.namespace = '.featured-collection-' + this.sectionId;
5074
5075 this.options = {
5076 scrollable: this.$container.data('scrollable'),
5077 paginate: this.$container.data('paginate'),
5078 instagram: this.$container.data('instagram')
5079 };
5080
5081 var paginateBy = this.$container.data('paginate-by');
5082 var productCount = this.$container.data('collection-count');
5083
5084 this.settings = {
5085 url: this.$container.data('collection-url'),
5086 page: 1,
5087 pageCount: this.options.paginate ? Math.ceil(productCount / paginateBy) : 0,
5088 itemsToScroll: 3
5089 };
5090
5091 this.state = {
5092 isInit: false,
5093 loading: false,
5094 scrollerEnabled: false,
5095 loadedAllProducts: false,
5096 scrollable: this.options.scrollable,
5097 scrollInterval: null,
5098 scrollSpeed: 3 // smaller is faster
5099 };
5100
5101 this.sizing = {
5102 scroller: 0,
5103 itemWidth: 0
5104 };
5105
5106 slate.utils.promiseStylesheet().then(function() {
5107 this.checkVisibility();
5108 $(window).on('scroll' + this.namespace, $.debounce(200, this.checkVisibility.bind(this)));
5109 }.bind(this));
5110 }
5111
5112 FeaturedCollection.prototype = $.extend({}, FeaturedCollection.prototype, {
5113 checkVisibility: function() {
5114 if (this.state.isInit) {
5115 // If a value is 0, we need to recalculate starting points
5116 if (this.sizing.scrollSize === 0) {
5117 this.$scrollWrap.trigger('scroll' + this.namespace);
5118 }
5119 $(window).off('scroll' + this.namespace);
5120 return;
5121 }
5122
5123 var visibleThreshold = this.options.instagram ? 500 : 0;
5124
5125 if (theme.isElementVisible(this.$container, visibleThreshold)) {
5126 if (this.options.instagram) {
5127 this.initInstagram();
5128 }
5129 this.init();
5130 this.state.isInit = true;
5131 }
5132 },
5133
5134 init: function() {
5135 new theme.HoverProductGrid(this.$container);
5136
5137 if (!this.state.scrollable) {
5138 return;
5139 }
5140
5141 this.sizing = this.getScrollWidths();
5142
5143 $(window).on('resize' + this.namespace, $.debounce(200, this.handleResize.bind(this)));
5144
5145 this.toggleScrollListener(this.state.scrollable);
5146 this.arrowListeners(this.state.scrollable);
5147 },
5148
5149 reInit: function() {
5150 new theme.HoverProductGrid(this.$container);
5151
5152 if (this.state.scrollable) {
5153 this.sizing = this.getScrollWidths();
5154 this.toggleScrollListener(this.state.scrollable);
5155 }
5156
5157 theme.reinitProductGridItem();
5158 },
5159
5160 initInstagram: function() {
5161 $('.instagram-image', this.$container).each(function(i, el) {
5162 if ($(el).is('[data-url]')){
5163 this.getInstagramImage($(el));
5164 }
5165 }.bind(this));
5166 },
5167
5168 getInstagramImage: function($el) {
5169 var instagramLink = $el.attr('data-url')
5170 var regex = /(instagram.com|instagr.am)\/p\/([^\/\s\?\#]+)/;
5171 if ( regex.test(instagramLink) ){
5172 var found = regex.exec(instagramLink);
5173 var shortcode = found.slice(-1).pop();
5174 var oembed_url = 'https://www.instagram.com/p/' + encodeURIComponent( shortcode ) + '/media/?size=l';
5175 var request = new XMLHttpRequest();
5176 request.open('GET', oembed_url, true);
5177 request.onload = function() {
5178 if (request.status >= 200 && request.status < 400) {
5179 $el.css('background-image', 'url(' + request.responseURL + ')' );
5180
5181 // Remove min-height now that some content is loaded
5182 if (!config.instagramLoaded) {
5183 $('.instagram-section__grid').css('min-height', '0');
5184 config.instagramLoaded = true;
5185 }
5186 } else {
5187 console.warn('Instagram request failed');
5188 }
5189 };
5190 request.onerror = function() {
5191 console.warn('Instagram request failed');
5192 };
5193 request.send();
5194 } else {
5195 $el
5196 .html('Invalid Instagram link. Use this format:<small>https://instagram.com/p/BeQb_V9jyDr</small>')
5197 .addClass('instagram-image--error');
5198 }
5199 },
5200
5201 loadingState: function(loading) {
5202 this.state.loading = loading;
5203 this.$container.toggleClass(classes.loading, loading);
5204 },
5205
5206 getScrollWidths: function() {
5207 var container = this.$scrollWrap.width();
5208 var scroller = this.$scrollWrap[0].scrollWidth;
5209 var itemWidth = this.$scrollWrap.find('.grid__item').first().outerWidth();
5210
5211 // First time this runs there is a 200px CSS animation that JS doesn't
5212 // take into account, so manually subtract from the scroller width
5213 if (!this.state.isInit) {
5214 scroller = scroller - 200;
5215 }
5216
5217 if (scroller <= container) {
5218 this.disableArrow(null, true);
5219 }
5220
5221 return {
5222 scroller: scroller,
5223 scrollSize: scroller - container,
5224 itemWidth: itemWidth
5225 };
5226 },
5227
5228 handleResize: function() {
5229 if (this.state.scrollable) {
5230 this.sizing = this.getScrollWidths();
5231 }
5232 this.toggleScrollListener(this.state.scrollable);
5233 this.arrowListeners(this.state.scrollable);
5234 },
5235
5236 toggleScrollListener: function(enable) {
5237 if (enable) {
5238 if (this.state.scrollerEnabled) { return; }
5239 this.$scrollWrap.on('scroll' + this.namespace, $.throttle(250, this.scrollCheck.bind(this)));
5240 this.state.scrollerEnabled = true;
5241 } else {
5242 this.$scrollWrap.off('scroll' + this.namespace);
5243 this.state.scrollerEnabled = false;
5244 }
5245 },
5246
5247 scrollCheck: function(evt) {
5248 if (this.state.loading) {
5249 this.toggleScrollListener(false);
5250 return;
5251 }
5252
5253 // If a value is 0, we need to recalculate starting points
5254 if (this.sizing.scrollSize === 0) {
5255 this.sizing = this.getScrollWidths();
5256 }
5257
5258 var scrollLeft = evt.currentTarget.scrollLeft ? evt.currentTarget.scrollLeft : 0;
5259 var percent = Math.floor(scrollLeft / this.sizing.scrollSize * 100);
5260 var fromEnd = this.sizing.scrollSize - scrollLeft;
5261
5262 if (this.options.paginate) {
5263 if (!this.state.loadedAllProducts && percent > 50) {
5264 this.getNewProducts();
5265 }
5266 }
5267
5268 if (!percent) {
5269 percent = 0;
5270 }
5271
5272 this.disableArrow(percent);
5273 },
5274
5275 arrowListeners: function(enable) {
5276 if (enable) {
5277 this.$scrollArrows
5278 .removeClass('hide')
5279 .off(this.namespace)
5280 .on('click' + this.namespace, this.arrowScroll.bind(this));
5281 } else {
5282 this.$scrollArrows
5283 .addClass('hide')
5284 .off(this.namespace);
5285 }
5286 },
5287
5288 arrowScroll: function(evt) {
5289 var direction = $(evt.currentTarget).hasClass(classes.arrowLeft) ? 'left' : 'right';
5290 var iteration = theme.config.bpSmall ? 1 : 2;
5291
5292 if (evt.type === 'mouseenter') {
5293 this.state.scrollInterval = setInterval(function(){
5294 var currentPos = this.$scrollWrap.scrollLeft();
5295 var newPos = direction === 'left' ? (currentPos - iteration) : (currentPos + iteration);
5296 this.$scrollWrap.scrollLeft(newPos);
5297 }.bind(this), this.state.scrollSpeed);
5298 } else if (evt.type === 'mouseleave') {
5299 clearInterval(this.state.scrollInterval);
5300 } else if (evt.type === 'click') {
5301 clearInterval(this.state.scrollInterval);
5302
5303 var currentPos = this.$scrollWrap.scrollLeft();
5304 var scrollAmount = this.sizing.itemWidth * this.settings.itemsToScroll;
5305 var newPos = direction === 'left' ? (currentPos - scrollAmount) : (currentPos + scrollAmount);
5306
5307 this.$scrollWrap.stop().animate({
5308 scrollLeft: newPos
5309 }, 400, 'swing');
5310 }
5311
5312 if (newPos <= 0) {
5313 this.disableArrow(newPos);
5314 }
5315 },
5316
5317 disableArrow: function(pos, all) {
5318 this.$scrollArrows
5319 .removeClass(classes.disableScrollRight)
5320 .removeClass(classes.disableScrollLeft);
5321
5322 if (all) {
5323 this.$scrollArrows
5324 .addClass(classes.disableScrollRight)
5325 .addClass(classes.disableScrollLeft);
5326 return;
5327 }
5328
5329 // Max left scroll
5330 if (pos <= 0) {
5331 this.$scrollArrows.addClass(classes.disableScrollLeft);
5332 return;
5333 }
5334
5335 // Max right scroll
5336 if (pos >= 96) {
5337 this.$scrollArrows.addClass(classes.disableScrollRight);
5338 return;
5339 }
5340 },
5341
5342 getNewProducts: function() {
5343 this.loadingState(true);
5344 var newPage = this.settings.page + 1;
5345
5346 // No more pages, disable features
5347 if (newPage > this.settings.pageCount) {
5348 this.loadingState(false);
5349 this.state.loadedAllProducts = true;
5350 return;
5351 }
5352
5353 var newUrl = this.settings.url + '?page=' + (newPage);
5354
5355 $.get(newUrl, function(data) {
5356 var $template = $(data);
5357 var $newProducts = $template.find(selectors.collectionProductContainer + ' .grid-product');
5358
5359 $(selectors.productContainer, this.$container).append($newProducts);
5360 this.ajaxSuccess();
5361 }.bind(this));
5362 },
5363
5364 ajaxSuccess: function() {
5365 this.loadingState(false);
5366 this.settings.page = this.settings.page + 1;
5367 this.reInit();
5368 },
5369
5370 forceReload: function() {
5371 this.onUnload();
5372 this.init();
5373 },
5374
5375 // Only runs in the editor while a user is activating.
5376 // Rearranges quick shop modals to fix potentially broken layout
5377 onLoad: function() {
5378 theme.QuickShopScreens.reInit(this.$container);
5379 },
5380
5381 onUnload: function() {
5382 $(window).off(this.namespace).trigger('resize');
5383 this.$scrollWrap.off(this.namespace);
5384 theme.QuickShopScreens.unload(this.$container);
5385 }
5386
5387 });
5388
5389 return FeaturedCollection;
5390 })();
5391
5392 theme.Collection = (function() {
5393 var isAnimating = false;
5394
5395 var classes = {
5396 tags: '.tags',
5397 activeTag: 'tag--active'
5398 };
5399
5400 function Collection(container) {
5401 this.container = container;
5402 this.sectionId = $(container).attr('data-section-id');
5403 this.namespace = '.collection-' + this.sectionId;
5404
5405 var hasHeroImage = $(container).find('.collection-hero').length;
5406
5407 if (hasHeroImage) {
5408 this.checkIfNeedReload();
5409 } else if (theme.settings.overlayHeader) {
5410 theme.headerNav.disableOverlayHeader();
5411 }
5412
5413 // Ajax pagination
5414 $(window).on('popstate', function(state) {
5415 if (state) {
5416
5417 // Bail if it's a hash link
5418 if(location.href.indexOf(location.pathname) >= 0) {
5419 return true;
5420 }
5421
5422 this.getNewCollectionContent(location.href);
5423 }
5424 }.bind(this));
5425
5426 this.init();
5427 }
5428
5429 Collection.prototype = $.extend({}, Collection.prototype, {
5430 init: function() {
5431 // init is called on load and when tags are selected
5432 this.$container = $(this.container);
5433 this.sectionId = this.$container.attr('data-section-id');
5434
5435 new theme.HoverProductGrid(this.$container);
5436 this.sortBy();
5437 this.sortTags();
5438 this.initTagAjax();
5439 },
5440
5441 initTagAjax: function() {
5442 this.$container.on('click' + this.namespace, '.tags a', function(evt) {
5443 var $el = $(evt.currentTarget);
5444 if ($el.hasClass('no-ajax')) {
5445 return;
5446 }
5447
5448 evt.preventDefault();
5449
5450 if (isAnimating) {
5451 return;
5452 }
5453
5454 isAnimating = true;
5455
5456 var newUrl = $el.attr('href');
5457 $(classes.tags).find('.' + classes.activeTag).removeClass(classes.activeTag);
5458 $el.parent().addClass(classes.activeTag);
5459 history.pushState({}, '', newUrl);
5460 $('.grid-product').addClass('unload');
5461 this.getNewCollectionContent(newUrl);
5462 }.bind(this));
5463 },
5464
5465 getNewCollectionContent: function(url) {
5466 url = url + '?view=ajax';
5467 $('#CollectionAjaxResult').load(url + ' #CollectionAjaxContent', function() {
5468 isAnimating = false;
5469 this.reInit();
5470 }.bind(this));
5471 },
5472
5473 sortBy: function() {
5474 var $sortBy = $('#SortBy');
5475
5476 if (!$sortBy.length) {
5477 return;
5478 }
5479
5480 $sortBy.on('change', function() {
5481 location.href = '?sort_by=' + $(this).val();
5482 });
5483 },
5484
5485 sortTags: function() {
5486 var $sortTags = $('#SortTags');
5487
5488 if (!$sortTags.length) {
5489 return;
5490 }
5491
5492 $sortTags.on('change', function() {
5493 location.href = $(this).val();
5494 });
5495 },
5496
5497 // A liquid variable in the header needs a full page refresh
5498 // if the collection header hero image setting is enabled
5499 // and the header is set to sticky. Only necessary in the editor.
5500 checkIfNeedReload: function() {
5501 if (!Shopify.designMode) {
5502 return;
5503 }
5504
5505 if (!theme.settings.overlayHeader) {
5506 return;
5507 }
5508
5509 if (!$('.header-wrapper').hasClass('header-wrapper--overlay')) {
5510 location.reload();
5511 }
5512 },
5513
5514 reInit: function() {
5515 sections.reinitSection('collection-template');
5516
5517 theme.reinitProductGridItem();
5518 },
5519
5520 forceReload: function() {
5521 this.onUnload();
5522 this.init();
5523 },
5524
5525 onUnload: function() {
5526 $(window).off(this.namespace);
5527 this.$container.off(this.namespace);
5528 }
5529
5530 });
5531
5532 return Collection;
5533 })();
5534
5535 theme.HeaderSection = (function() {
5536
5537 function Header(container) {
5538 var $container = this.$container = $(container);
5539 var sectionId = this.sectionId = $container.attr('data-section-id');
5540
5541 this.initDrawers();
5542 theme.headerNav.init();
5543 theme.slideNav.init();
5544
5545 // Reload any slideshow when the header is reloaded to make sure the
5546 // sticky header works as expected (it can be anywhere in the sections.instance array)
5547 sections.reinitSection('slideshow-section');
5548 }
5549
5550 Header.prototype = $.extend({}, Header.prototype, {
5551 initDrawers: function() {
5552 if ($(document.body).hasClass('template-cart')) {
5553 new theme.AjaxCart('CartPage');
5554 } else if (theme.settings.cartType === 'drawer') {
5555 new theme.AjaxCart('CartDrawer');
5556 }
5557 },
5558
5559 onUnload: function() {
5560 theme.headerNav.unload();
5561 theme.slideNav.unload();
5562 }
5563 });
5564
5565 return Header;
5566
5567 })();
5568
5569 theme.FeaturedContentSection = (function() {
5570
5571 function FeaturedContent() {
5572 $('.rte').find('a:not(:has(img))').addClass('text-link');
5573 }
5574
5575 return FeaturedContent;
5576 })();
5577
5578 theme.slideshows = {};
5579
5580 theme.SlideshowSection = (function() {
5581
5582 function SlideshowSection(container) {
5583 var $container = this.$container = $(container);
5584 var $section = $container.parent();
5585 var sectionId = $container.attr('data-section-id');
5586 var slideshow = this.slideshow = '#Slideshow-' + sectionId;
5587
5588 var $imageContainer = $(container).find('.hero');
5589 if ($imageContainer.length) {
5590 theme.loadImageSection($imageContainer);
5591 }
5592
5593 this.init();
5594 }
5595
5596 SlideshowSection.prototype = $.extend({}, SlideshowSection.prototype, {
5597 init: function() {
5598 var args = {
5599 arrows: $(this.slideshow).data('arrows'),
5600 dots: $(this.slideshow).data('dots'),
5601 pauseOnHover: true
5602 };
5603
5604 theme.slideshows[this.slideshow] = new theme.Slideshow(this.slideshow, args);
5605 },
5606
5607 forceReload: function() {
5608 this.onUnload();
5609 this.init();
5610 },
5611
5612 onUnload: function() {
5613 theme.slideshows[this.slideshow].destroy();
5614 delete theme.slideshows[this.slideshow];
5615 },
5616
5617 onSelect: function() {
5618 $(this.slideshow).slick('slickPause');
5619 },
5620
5621 onDeselect: function() {
5622 $(this.slideshow).slick('slickPlay');
5623 },
5624
5625 onBlockSelect: function(evt) {
5626 var $slideshow = $(this.slideshow);
5627
5628 // Ignore the cloned version
5629 var $slide = $('.slideshow__slide--' + evt.detail.blockId + ':not(.slick-cloned)');
5630 var slideIndex = $slide.data('slick-index');
5631
5632 // Go to selected slide, pause autoplay
5633 $slideshow.slick('slickGoTo', slideIndex).slick('slickPause');
5634 },
5635
5636 onBlockDeselect: function() {
5637 $(this.slideshow).slick('slickPlay');
5638 }
5639 });
5640
5641 return SlideshowSection;
5642 })();
5643
5644 theme.HeroAnimated = (function() {
5645
5646 var classes = {
5647 active: 'animated__slide--active',
5648 inactive: 'animated__slide--inactive'
5649 }
5650
5651 function HeroAnimated(container) {
5652 var $container = this.$container = $(container);
5653 var $section = $container.parent();
5654 var sectionId = $container.attr('data-section-id');
5655 var imageCount = $container.data('count');
5656 var namespace = '.hero-animated-' + sectionId;
5657
5658 var $imageContainer = $(container).find('.hero');
5659 if ($imageContainer.length) {
5660 theme.loadImageSection($imageContainer);
5661 }
5662 this.$allImages = $container.find('.animated__slide');
5663
5664 this.state = {
5665 active: false,
5666 activeIndex: 0
5667 };
5668
5669 if (imageCount === 1) {
5670 this.setFades(true);
5671 return;
5672 }
5673
5674 this.interval;
5675 this.intervalSpeed = $container.data('interval');
5676 this.maxIndex = imageCount - 1;
5677
5678 slate.utils.promiseStylesheet().then(function() {
5679 this.checkVisibility();
5680 $(window).on('scroll' + namespace, $.throttle(300, this.checkVisibility.bind(this)));
5681 }.bind(this));
5682 }
5683
5684 HeroAnimated.prototype = $.extend({}, HeroAnimated.prototype, {
5685 checkVisibility: function() {
5686 if (!theme.isElementVisible(this.$container)) {
5687 this.state.active = false;
5688 clearInterval(this.interval);
5689 return;
5690 }
5691
5692 if (this.state.active) {
5693 return;
5694 }
5695
5696 this.initInterval();
5697 },
5698
5699 initInterval: function() {
5700 this.state.active = true;
5701
5702 this.setFades(true);
5703 this.interval = setInterval(function() {
5704 this.setFades();
5705 }.bind(this), this.intervalSpeed);
5706 },
5707
5708 setFades: function(first) {
5709 // Get next image index
5710 var nextIndex = this.state.activeIndex === this.maxIndex ? 0 : this.state.activeIndex + 1;
5711
5712 if (first) {
5713 nextIndex = this.state.activeIndex;
5714 }
5715
5716 // Unset existing image
5717 if (!first) {
5718 this.$allImages.eq(this.state.activeIndex)
5719 .removeClass(classes.active)
5720 .addClass(classes.inactive);
5721 }
5722
5723 // Set next image as active
5724 this.$allImages.eq(nextIndex)
5725 .removeClass(classes.inactive)
5726 .addClass(classes.active);
5727
5728 this.state.activeIndex = nextIndex;
5729 },
5730
5731 onUnload: function() {
5732 clearInterval(this.interval);
5733 }
5734 });
5735
5736 return HeroAnimated;
5737 })();
5738
5739 theme.VideoSection = (function() {
5740 var youtubeReady;
5741 var videos = [];
5742 var youtubePlayers = [];
5743 var youtubeVideoOptions = {
5744 width: 1280,
5745 height: 720,
5746 playerVars: {
5747 autohide: 0,
5748 branding: 0,
5749 cc_load_policy: 0,
5750 controls: 0,
5751 fs: 0,
5752 iv_load_policy: 3,
5753 modestbranding: 1,
5754 playsinline: 1,
5755 quality: 'hd720',
5756 rel: 0,
5757 showinfo: 0,
5758 wmode: 'opaque'
5759 },
5760 events: {
5761 onReady: onVideoPlayerReady,
5762 onStateChange: onVideoStateChange
5763 }
5764 };
5765
5766 var vimeoReady = false;
5767 var vimeoVideoOptions = {
5768 byline: false,
5769 title: false,
5770 portrait: false,
5771 loop: true
5772 };
5773
5774 var selectors = {
5775 videoParent: '.video-parent-section'
5776 };
5777
5778 var classes = {
5779 loading: 'loading',
5780 loaded: 'loaded',
5781 interactable: 'video-interactable'
5782 };
5783
5784 function videoSection(container) {
5785 var $container = this.$container = $(container);
5786 var sectionId = this.sectionId = $container.attr('data-section-id');
5787 var youtubePlayerId = this.youtubePlayerId = 'YouTubeVideo-' + this.sectionId;
5788 this.namespace = '.' + youtubePlayerId;
5789 var vimeoPlayerId = this.vimeoPlayerId = 'Vimeo-' + this.sectionId;
5790 var $vimeoTrigger = this.$vimeoTrigger = $('#VimeoTrigger-' + this.sectionId);
5791 var mp4Video = 'Mp4Video-' + this.sectionId;
5792
5793 var $youtubeDiv = $('#' + youtubePlayerId);
5794 var $vimeoDiv = $('#' + vimeoPlayerId);
5795 var $mp4Div = $('#' + mp4Video);
5796
5797 this.vimeoPlayer = [];
5798
5799 if ($youtubeDiv.length) {
5800 this.youtubeVideoId = $youtubeDiv.data('video-id');
5801 this.initYoutubeVideo();
5802 }
5803
5804 if ($vimeoDiv.length) {
5805 this.vimeoVideoId = $vimeoDiv.data('video-id');
5806 this.initVimeoVideo();
5807 }
5808
5809 if ($mp4Div.length) {
5810 startMp4Playback(mp4Video).then(function() {
5811 // Video played as expected
5812 setParentAsLoaded($mp4Div);
5813 }).catch(function(error) {
5814 // Video cannot be played with autoplay, so let
5815 // user interact with video element itself
5816 setParentAsLoaded($mp4Div);
5817 setVideoToBeInteractedWith($mp4Div);
5818 })
5819 }
5820 }
5821
5822 function startMp4Playback(mp4Video) {
5823 return document.querySelector('#' + mp4Video).play();
5824 }
5825
5826 function onVideoPlayerReady(evt) {
5827 var $player = $(evt.target.a);
5828 var playerId = $player.attr('id');
5829 youtubePlayers[playerId] = evt.target; // update stored player
5830 var player = youtubePlayers[playerId];
5831
5832 setParentAsLoading($player);
5833
5834 youtubePlayers[playerId].mute();
5835
5836 // Remove from tabindex because YouTube iframes are annoying and you can focus
5837 // on the YouTube logo and it breaks
5838 $player.attr('tabindex', '-1');
5839
5840 // Play video if in view
5841 slate.utils.promiseStylesheet().then(function() {
5842 videoVisibilityCheck(playerId);
5843
5844 // Add out of view pausing
5845 $(window).on('scroll.' + playerId, {id: playerId}, $.throttle(150, videoVisibilityCheck));
5846 });
5847 }
5848
5849 function videoVisibilityCheck(id) {
5850 var playerId;
5851
5852 if (!id) {
5853 return;
5854 }
5855
5856 if (typeof id === 'string') {
5857 playerId = id;
5858 } else {
5859 // Data comes in as part of the scroll event
5860 if (id && id.data) {
5861 playerId = id.data.id;
5862 } else {
5863 return;
5864 }
5865 }
5866
5867 if (theme.isElementVisible($('#' + playerId))) {
5868 playVisibleVideo(playerId);
5869 } else {
5870 pauseHiddenVideo(playerId);
5871 }
5872 }
5873
5874 function playVisibleVideo(id) {
5875 if (youtubePlayers[id] && typeof youtubePlayers[id].playVideo === 'function') {
5876 youtubePlayers[id].playVideo();
5877 }
5878 }
5879
5880 function pauseHiddenVideo(id) {
5881 if (youtubePlayers[id] && typeof youtubePlayers[id].pauseVideo === 'function') {
5882 youtubePlayers[id].pauseVideo();
5883 }
5884 }
5885
5886 function onVideoStateChange(evt) {
5887 var $player = $(evt.target.a);
5888 var playerId = $player.attr('id');
5889 var player = youtubePlayers[playerId];
5890
5891 switch (evt.data) {
5892 case -1: // unstarted
5893 // Handle low power state on iOS by checking if
5894 // video is reset to unplayed after attempting to buffer
5895 if (videos[playerId].attemptedToPlay) {
5896 setParentAsLoaded($player);
5897 setVideoToBeInteractedWith($player);
5898 }
5899 break;
5900 case 0: // ended
5901 player.playVideo();
5902 break;
5903 case 1: // playing
5904 setParentAsLoaded($player);
5905 break;
5906 case 3: // buffering
5907 videos[playerId].attemptedToPlay = true;
5908 break;
5909 }
5910 }
5911
5912 function setParentAsLoading($el) {
5913 $el
5914 .closest(selectors.videoParent)
5915 .addClass(classes.loading);
5916 }
5917
5918 function setParentAsLoaded($el) {
5919 $el
5920 .closest(selectors.videoParent)
5921 .removeClass(classes.loading)
5922 .addClass(classes.loaded);
5923 }
5924
5925 function setVideoToBeInteractedWith($el) {
5926 $el
5927 .closest(selectors.videoParent)
5928 .addClass(classes.interactable);
5929 }
5930
5931 videoSection.prototype = $.extend({}, videoSection.prototype, {
5932 initYoutubeVideo: function() {
5933 videos[this.youtubePlayerId] = {
5934 id: this.youtubePlayerId,
5935 videoId: this.youtubeVideoId,
5936 type: 'youtube',
5937 attemptedToPlay: false
5938 };
5939
5940 if (!youtubeReady) {
5941 window.loadYouTube();
5942 $('body').on('youTubeReady' + this.namespace, this.loadYoutubeVideo.bind(this));
5943 } else {
5944 this.loadYoutubeVideo();
5945 }
5946 },
5947
5948 loadYoutubeVideo: function() {
5949 var args = $.extend({}, youtubeVideoOptions, videos[this.youtubePlayerId]);
5950 args.playerVars.controls = 0;
5951 youtubePlayers[this.youtubePlayerId] = new YT.Player(this.youtubePlayerId, args);
5952
5953 youtubeReady = true;
5954 },
5955
5956 initVimeoVideo: function() {
5957 videos[this.vimeoPlayerId] = {
5958 divId: this.vimeoPlayerId,
5959 id: this.vimeoVideoId,
5960 type: 'vimeo'
5961 };
5962
5963 var $player = $('#' + this.vimeoPlayerId);
5964 setParentAsLoading($player);
5965
5966 // Button to play video on mobile
5967 this.$vimeoTrigger.on('click', + this.namespace, function(evt) {
5968 // $(evt.currentTarget).addClass('hide');
5969 this.requestToPlayVimeoVideo(this.vimeoPlayerId);
5970 }.bind(this));
5971
5972 if (!vimeoReady) {
5973 window.loadVimeo();
5974 $('body').on('vimeoReady' + this.namespace, this.loadVimeoVideo.bind(this));
5975 } else {
5976 this.loadVimeoVideo();
5977 }
5978 },
5979
5980 loadVimeoVideo: function() {
5981 var args = $.extend({}, vimeoVideoOptions, videos[this.vimeoPlayerId]);
5982 this.vimeoPlayer[this.vimeoPlayerId] = new Vimeo.Player(videos[this.vimeoPlayerId].divId, args);
5983
5984 vimeoReady = true;
5985
5986 // Only autoplay on larger screens
5987 if (!theme.config.bpSmall) {
5988 this.requestToPlayVimeoVideo(this.vimeoPlayerId);
5989 } else {
5990 var $player = $('#' + this.vimeoPlayerId);
5991 setParentAsLoaded($player);
5992 }
5993 },
5994
5995 requestToPlayVimeoVideo: function(id) {
5996 // The slider may initialize and attempt to play the video before
5997 // the API is even ready, because it sucks.
5998
5999 if (!vimeoReady) {
6000 // Wait for the trigger, then play it
6001 $('body').on('vimeoReady' + this.namespace, function() {
6002 this.playVimeoVideo(id);
6003 }.bind(this))
6004 return;
6005 }
6006
6007 this.playVimeoVideo(id);
6008 },
6009
6010 playVimeoVideo: function(id) {
6011 this.vimeoPlayer[id].play();
6012 this.vimeoPlayer[id].setVolume(0);
6013
6014 var $player = $('#' + id);
6015 setParentAsLoaded($player);
6016 },
6017
6018 onUnload: function(evt) {
6019 var sectionId = evt.target.id.replace('shopify-section-', '');
6020 var playerId = 'YouTubeVideo-' + sectionId;
6021 if (youtubePlayers[playerId]) {
6022 youtubePlayers[playerId].destroy();
6023 }
6024 $(window).off('scroll' + this.namespace);
6025 $('body').off('vimeoReady' + this.namespace);
6026 }
6027 });
6028
6029 return videoSection;
6030 })();
6031
6032 theme.Testimonials = (function() {
6033 var slideCount = 0;
6034 var defaults = {
6035 accessibility: true,
6036 arrows: false,
6037 dots: true,
6038 autoplay: false,
6039 touchThreshold: 20,
6040 slidesToShow: 3,
6041 slidesToScroll: 3
6042 };
6043
6044 function Testimonials(container) {
6045 var $container = this.$container = $(container);
6046 var sectionId = $container.attr('data-section-id');
6047 var wrapper = this.wrapper = '.testimonials-wrapper';
6048 var slider = this.slider = '#Testimonials-' + sectionId;
6049 var $slider = $(slider);
6050
6051 this.sliderActive = false;
6052 var mobileOptions = $.extend({}, defaults, {
6053 slidesToShow: 1,
6054 slidesToScroll: 1,
6055 adaptiveHeight: true
6056 });
6057
6058 slideCount = $slider.data('count');
6059
6060 // Override slidesToShow/Scroll if there are not enough blocks
6061 if (slideCount < defaults.slidesToShow) {
6062 defaults.slidesToShow = slideCount;
6063 defaults.slidesToScroll = slideCount;
6064 }
6065
6066 $slider.on('init', this.a11y.bind(this));
6067
6068 if (theme.config.bpSmall) {
6069 this.init($slider, mobileOptions);
6070 } else {
6071 this.init($slider, defaults);
6072 }
6073
6074 $('body').on('matchSmall', function() {
6075 this.init($slider, mobileOptions);
6076 }.bind(this));
6077
6078 $('body').on('matchLarge', function() {
6079 this.init($slider, defaults);
6080 }.bind(this));
6081 }
6082
6083 Testimonials.prototype = $.extend({}, Testimonials.prototype, {
6084 onUnload: function() {
6085 $(this.slider, this.wrapper).slick('unslick');
6086 },
6087
6088 onBlockSelect: function(evt) {
6089 // Ignore the cloned version
6090 var $slide = $('.testimonials-slide--' + evt.detail.blockId + ':not(.slick-cloned)');
6091 var slideIndex = $slide.data('slick-index');
6092
6093 // Go to selected slide, pause autoplay
6094 $(this.slider, this.wrapper).slick('slickGoTo', slideIndex);
6095 },
6096
6097 init: function(obj, args) {
6098 if (this.sliderActive) {
6099 obj.slick('unslick');
6100 this.sliderActive = false;
6101 }
6102
6103 obj.slick(args);
6104 this.sliderActive = true;
6105
6106 if (AOS) {
6107 AOS.refresh();
6108 }
6109 },
6110
6111 a11y: function(event, obj) {
6112 var $list = obj.$list;
6113 var $wrapper = $(this.wrapper, this.$container);
6114
6115 // Remove default Slick aria-live attr until slider is focused
6116 $list.removeAttr('aria-live');
6117
6118 // When an element in the slider is focused set aria-live
6119 $wrapper.on('focusin', function(evt) {
6120 if ($wrapper.has(evt.target).length) {
6121 $list.attr('aria-live', 'polite');
6122 }
6123 });
6124
6125 // Remove aria-live
6126 $wrapper.on('focusout', function(evt) {
6127 if ($wrapper.has(evt.target).length) {
6128 $list.removeAttr('aria-live');
6129 }
6130 });
6131 }
6132 });
6133
6134 return Testimonials;
6135 })();
6136
6137 theme.NewsletterPopup = (function() {
6138 function NewsletterPopup(container) {
6139 var $container = this.$container = $(container);
6140 var sectionId = $container.attr('data-section-id');
6141 this.cookieName = 'newsletter-' + sectionId;
6142
6143 if (!$container.length) {
6144 return;
6145 }
6146
6147 // Prevent popup on Shopify robot challenge page
6148 if (window.location.pathname === '/challenge') {
6149 return;
6150 }
6151
6152 this.data = {
6153 secondsBeforeShow: $container.data('delay-seconds'),
6154 daysBeforeReappear: $container.data('delay-days'),
6155 cookie: Cookies.get(this.cookieName),
6156 testMode: $container.data('test-mode')
6157 };
6158
6159 this.modal = new theme.Modals('NewsletterPopup-' + sectionId, 'newsletter-popup-modal');
6160
6161 // Open modal if errors or success message exist
6162 if ($container.find('.errors').length || $container.find('.note--success').length) {
6163 this.modal.open();
6164 }
6165
6166 // Set cookie as opened if success message
6167 if ($container.find('.note--success').length) {
6168 this.closePopup(true);
6169 return;
6170 }
6171
6172 $('body').on('modalClose.' + $container.attr('id'), this.closePopup.bind(this));
6173
6174 if (!this.data.cookie || this.data.testMode) {
6175 this.initPopupDelay();
6176 }
6177 }
6178
6179 NewsletterPopup.prototype = $.extend({}, NewsletterPopup.prototype, {
6180 initPopupDelay: function() {
6181 setTimeout(function() {
6182 this.modal.open();
6183 }.bind(this), this.data.secondsBeforeShow * 1000);
6184 },
6185
6186 closePopup: function(success) {
6187 // Remove a cookie in case it was set in test mode
6188 if (this.data.testMode) {
6189 Cookies.remove(this.cookieName, { path: '/' });
6190 return;
6191 }
6192
6193 var expiry = success ? 200 : this.data.daysBeforeReappear;
6194
6195 Cookies.set(this.cookieName, 'opened', { path: '/', expires: expiry });
6196 },
6197
6198 onLoad: function() {
6199 this.modal.open();
6200 },
6201
6202 onSelect: function() {
6203 this.modal.open();
6204 },
6205
6206 onDeselect: function() {
6207 this.modal.close();
6208 },
6209
6210 onUnload: function() {}
6211 });
6212
6213 return NewsletterPopup;
6214 })();
6215
6216 theme.Maps = (function() {
6217 var config = {
6218 zoom: 14
6219 };
6220 var apiStatus = null;
6221 var mapsToLoad = [];
6222
6223 var errors = {
6224 addressNoResults: theme.strings.addressNoResults,
6225 addressQueryLimit: theme.strings.addressQueryLimit,
6226 addressError: theme.strings.addressError,
6227 authError: theme.strings.authError
6228 };
6229
6230 var selectors = {
6231 section: '[data-section-type="map"]',
6232 map: '[data-map]',
6233 mapOverlay: '[data-map-overlay]'
6234 };
6235
6236 var classes = {
6237 mapError: 'map-section--load-error',
6238 errorMsg: 'map-section__error errors text-center'
6239 };
6240
6241 // Global function called by Google on auth errors.
6242 // Show an auto error message on all map instances.
6243 window.gm_authFailure = function() {
6244 if (!Shopify.designMode) {
6245 return;
6246 }
6247
6248 $(selectors.section).addClass(classes.mapError);
6249 $(selectors.map).remove();
6250 $(selectors.mapOverlay).after(
6251 '<div class="' +
6252 classes.errorMsg +
6253 '">' +
6254 theme.strings.authError +
6255 '</div>'
6256 );
6257 };
6258
6259 function Map(container) {
6260 this.$container = $(container);
6261 this.sectionId = this.$container.attr('data-section-id');
6262 this.namespace = '.map-' + this.sectionId;
6263 this.$map = this.$container.find(selectors.map);
6264 this.key = this.$map.data('api-key');
6265
6266 if (!this.key) {
6267 return;
6268 }
6269
6270 // Lazyload API
6271 this.checkVisibility();
6272 $(window).on('scroll' + this.namespace, $.throttle(50, this.checkVisibility.bind(this)));
6273 }
6274
6275 function initAllMaps() {
6276 // API has loaded, load all Map instances in queue
6277 $.each(mapsToLoad, function(index, instance) {
6278 instance.createMap();
6279 });
6280 }
6281
6282 function geolocate($map) {
6283 var deferred = $.Deferred();
6284 var geocoder = new google.maps.Geocoder();
6285 var address = $map.data('address-setting');
6286
6287 geocoder.geocode({ address: address }, function(results, status) {
6288 if (status !== google.maps.GeocoderStatus.OK) {
6289 deferred.reject(status);
6290 }
6291
6292 deferred.resolve(results);
6293 });
6294
6295 return deferred;
6296 }
6297
6298 Map.prototype = $.extend({}, Map.prototype, {
6299 prepMapApi: function() {
6300 if (apiStatus === 'loaded') {
6301 this.createMap();
6302 } else {
6303 mapsToLoad.push(this);
6304
6305 if (apiStatus !== 'loading') {
6306 apiStatus = 'loading';
6307 if (typeof window.google === 'undefined' || typeof window.google.maps === 'undefined' ) {
6308 $.getScript(
6309 'https://maps.googleapis.com/maps/api/js?key=' + this.key
6310 ).then(function() {
6311 apiStatus = 'loaded';
6312 initAllMaps();
6313 });
6314 }
6315 }
6316 }
6317 },
6318
6319 createMap: function() {
6320 var $map = this.$map;
6321
6322 return geolocate($map)
6323 .then(
6324 function(results) {
6325 var mapOptions = {
6326 zoom: config.zoom,
6327 backgroundColor: 'none',
6328 center: results[0].geometry.location,
6329 draggable: false,
6330 clickableIcons: false,
6331 scrollwheel: false,
6332 disableDoubleClickZoom: true,
6333 disableDefaultUI: true
6334 };
6335
6336 var map = (this.map = new google.maps.Map($map[0], mapOptions));
6337 var center = (this.center = map.getCenter());
6338
6339 var marker = new google.maps.Marker({
6340 map: map,
6341 position: map.getCenter()
6342 });
6343
6344 google.maps.event.addDomListener(
6345 window,
6346 'resize',
6347 $.debounce(250, function() {
6348 google.maps.event.trigger(map, 'resize');
6349 map.setCenter(center);
6350 $map.removeAttr('style');
6351 })
6352 );
6353 }.bind(this)
6354 )
6355 .fail(function() {
6356 var errorMessage;
6357
6358 switch (status) {
6359 case 'ZERO_RESULTS':
6360 errorMessage = errors.addressNoResults;
6361 break;
6362 case 'OVER_QUERY_LIMIT':
6363 errorMessage = errors.addressQueryLimit;
6364 break;
6365 case 'REQUEST_DENIED':
6366 errorMessage = errors.authError;
6367 break;
6368 default:
6369 errorMessage = errors.addressError;
6370 break;
6371 }
6372
6373 // Show errors only to merchant in the editor.
6374 if (Shopify.designMode) {
6375 $map
6376 .parent()
6377 .addClass(classes.mapError)
6378 .html(
6379 '<div class="' +
6380 classes.errorMsg +
6381 '">' +
6382 errorMessage +
6383 '</div>'
6384 );
6385 }
6386 });
6387 },
6388
6389 checkVisibility: function() {
6390 if (theme.isElementVisible(this.$container, 600)) {
6391 this.prepMapApi();
6392 $(window).off(this.namespace);
6393 }
6394 },
6395
6396 onUnload: function() {
6397 if (this.$map.length === 0) {
6398 return;
6399 }
6400 // Causes a harmless JS error when a section without an active map is reloaded
6401 google.maps.event.clearListeners(this.map, 'resize');
6402 }
6403 });
6404
6405 return Map;
6406 })();
6407
6408 theme.Blog = (function() {
6409
6410 function Blog(container) {
6411 this.tagFilters();
6412 }
6413
6414 Blog.prototype = $.extend({}, Blog.prototype, {
6415 tagFilters: function() {
6416 var $filterBy = $('#BlogTagFilter');
6417
6418 if (!$filterBy.length) {
6419 return;
6420 }
6421
6422 $filterBy.on('change', function() {
6423 location.href = $(this).val();
6424 });
6425 },
6426
6427 onUnload: function() {
6428
6429 }
6430 });
6431
6432 return Blog;
6433 })();
6434
6435 theme.Photoswipe = (function() {
6436 var selectors = {
6437 trigger: '.product__photo-zoom',
6438 images: '.photoswipe__image',
6439 activeImage: '.slick-active .photoswipe__image'
6440 };
6441
6442 function Photoswipe($container, sectionId) {
6443 this.$container = $container;
6444 this.sectionId = sectionId;
6445 this.namespace = '.photoswipe-' + this.sectionId;
6446 this.gallery;
6447 this.$images;
6448 this.inSlideshow = false;
6449
6450 if ($container.attr('data-zoom') === 'false') {
6451 return;
6452 }
6453
6454 if ($container.attr('data-has-slideshow') === 'true') {
6455 this.inSlideshow = true;
6456 }
6457
6458 this.init();
6459 }
6460
6461 Photoswipe.prototype = $.extend({}, Photoswipe.prototype, {
6462 init: function() {
6463 var $trigger = this.$container.find(selectors.trigger);
6464 this.$images = this.$container.find(selectors.images);
6465 var items = [];
6466
6467 // Init gallery on active image
6468 $trigger.on('click' + this.namespace, function(evt) {
6469 items = this.getImageData();
6470 if (this.inSlideshow || theme.config.bpSmall) {
6471 var index = this.$container.find(selectors.activeImage).data('index');
6472 } else {
6473 var index = $(evt.currentTarget).data('index');
6474 }
6475 this.initGallery(items, index);
6476 }.bind(this));
6477 },
6478
6479 getImageData: function() {
6480 var haveImages = false;
6481 var items = [];
6482 var options = {};
6483
6484 this.$images.each(function() {
6485 var haveImages = true;
6486 var smallSrc = $(this).prop('currentSrc') || $(this).prop('src');
6487 var item = {
6488 msrc: smallSrc,
6489 src: $(this).data('photoswipe-src'),
6490 w: $(this).data('photoswipe-width'),
6491 h: $(this).data('photoswipe-height'),
6492 el: $(this)[0],
6493 initialZoomLevel: 0.5
6494 };
6495
6496 items.push(item);
6497 });
6498
6499 return items;
6500 },
6501
6502 initGallery: function(items, index) {
6503 var pswpElement = document.querySelectorAll('.pswp')[0];
6504
6505 var options = {
6506 allowPanToNext: false,
6507 captionEl: false,
6508 closeOnScroll: false,
6509 counterEl: false,
6510 history: false,
6511 index: index - 1,
6512 pinchToClose: false,
6513 preloaderEl: false,
6514 scaleMode: 'zoom',
6515 shareEl: false,
6516 tapToToggleControls: false,
6517 getThumbBoundsFn: function(index) {
6518 var pageYScroll = window.pageYOffset || document.documentElement.scrollTop;
6519 var thumbnail = items[index].el;
6520 var rect = thumbnail.getBoundingClientRect();
6521 return {x:rect.left, y:rect.top + pageYScroll, w:rect.width};
6522 }
6523 }
6524
6525 this.gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options);
6526
6527 this.gallery.init();
6528 this.gallery.listen('afterChange', this.afterChange.bind(this));
6529
6530 // If need to destroy it
6531 // this.gallery.destroy();
6532 },
6533
6534 afterChange: function() {
6535 if (this.inSlideshow) {
6536 var $slideshow = $('#ProductPhotos-' + this.sectionId);
6537 if ($slideshow.hasClass('slick-initialized')) {
6538 var newIndex = this.gallery.getCurrentIndex();
6539 $slideshow.slick('slickGoTo', newIndex);
6540 }
6541 }
6542 }
6543 });
6544
6545 return Photoswipe;
6546 })();
6547
6548
6549
6550 // Breakpoint values are used throughout many templates.
6551 // We strongly suggest not changing them globally.
6552 theme.bp = {};
6553 theme.bp.smallUp = 769;
6554 theme.bp.small = theme.bp.smallUp - 1;
6555
6556 theme.config = {
6557 cssLoaded: false,
6558 bpSmall: false,
6559 hasSessionStorage: true,
6560 mediaQuerySmall: 'screen and (max-width: '+ theme.bp.small +'px)',
6561 mediaQuerySmallUp: 'screen and (min-width: '+ theme.bp.smallUp +'px)',
6562 youTubeReady: false,
6563 vimeoReady: false,
6564 vimeoLoading: false,
6565 isSafari: !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/),
6566 isTouch: ('ontouchstart' in window) || window.DocumentTouch && window.document instanceof DocumentTouch || window.navigator.maxTouchPoints || window.navigator.msMaxTouchPoints ? true : false
6567 };
6568
6569 window.onYouTubeIframeAPIReady = function() {
6570 theme.config.youTubeReady = true;
6571 $('body').trigger('youTubeReady');
6572 };
6573
6574 window.loadYouTube = function() {
6575 if (theme.config.youtubeReady) {
6576 return;
6577 }
6578
6579 var tag = document.createElement('script');
6580 tag.src = "https://www.youtube.com/iframe_api";
6581 var firstScriptTag = document.getElementsByTagName('script')[0];
6582 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
6583 };
6584
6585 window.loadVimeo = function() {
6586 if (theme.config.vimeoLoading) {
6587 return;
6588 }
6589
6590 if (!theme.config.vimeoReady) {
6591 theme.config.vimeoLoading = true;
6592 var tag = document.createElement('script');
6593 tag.src = "https://player.vimeo.com/api/player.js";
6594 var firstScriptTag = document.getElementsByTagName('script')[0];
6595 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
6596
6597 // Because there's no way to check for the Vimeo API being loaded
6598 // asynchronously, we use this terrible timeout to wait for it being ready
6599 checkIfVimeoIsReady()
6600 .then(function() {
6601 theme.config.vimeoReady = true;
6602 theme.config.vimeoLoading = false;
6603 $('body').trigger('vimeoReady');
6604 })
6605 .fail(function() {
6606 // No vimeo API to talk to
6607 });
6608 }
6609 };
6610
6611 function checkIfVimeoIsReady() {
6612 var deferred = $.Deferred();
6613 var wait;
6614 var timeout;
6615
6616 wait = setInterval(function() {
6617 if (!Vimeo) {
6618 return;
6619 }
6620
6621 clearInterval(wait);
6622 clearTimeout(timeout);
6623 deferred.resolve();
6624 }, 500);
6625
6626 timeout = setTimeout(function() {
6627 clearInterval(wait);
6628 deferred.reject();
6629 }, 4000); // subjective. test up to 8 times over 4 seconds
6630
6631 return deferred;
6632 };
6633
6634 theme.init = function() {
6635 theme.setGlobals();
6636 theme.pageTransitions();
6637 theme.QuickShopScreens.init();
6638 theme.articleImages.init();
6639 theme.collapsibles.init();
6640 if (theme.settings.cartType === 'sticky') {
6641 new theme.StickyCart.init();
6642 }
6643 theme.customerTemplates.init();
6644 if (theme.settings.currenciesEnabled) {
6645 theme.currencySwitcher.init();
6646 }
6647 theme.videoModal();
6648 slate.rte.init();
6649
6650 $(document.documentElement).on('keyup.tab', function(evt) {
6651 if (evt.keyCode === 9) {
6652 $(document.documentElement).addClass('tab-outline');
6653 $(document.documentElement).off('keyup.tab');
6654 }
6655 });
6656
6657 // Two ways to determine if page was loaded from cache from back button
6658 // Most use `pageshow` + evt.persisted, Chrome uses `performance.navigation.type`
6659 window.addEventListener('pageshow', function(evt) {
6660 if (evt.persisted) {
6661 theme.refreshCart();
6662 }
6663 });
6664
6665 if (performance && performance.navigation.type === 2) {
6666 theme.refreshCart();
6667 }
6668 };
6669
6670 theme.refreshCart = function() {
6671 if (theme.settings.cartType === 'sticky' && theme.StickyCart) {
6672 $.getJSON('/cart.js').then(function(cart) {
6673 theme.StickyCart.refresh(cart);
6674 })
6675 }
6676 };
6677
6678 theme.setGlobals = function() {
6679 theme.config.hasSessionStorage = theme.isSessionStorageSupported();
6680
6681 if (theme.config.isTouch) {
6682 $('body').addClass('supports-touch');
6683 }
6684
6685 enquire.register(theme.config.mediaQuerySmall, {
6686 match: function() {
6687 theme.config.bpSmall = true;
6688 $('body').trigger('matchSmall');
6689 },
6690 unmatch: function() {
6691 theme.config.bpSmall = false;
6692 $('body').trigger('unmatchSmall');
6693 }
6694 });
6695
6696 enquire.register(theme.config.mediaQuerySmallUp, {
6697 match: function() {
6698 $('body').trigger('matchLarge');
6699 },
6700 unmatch: function() {
6701 $('body').trigger('unmatchLarge');
6702 }
6703 });
6704 };
6705
6706 theme.loadImageSection = function($container) {
6707 // Wait until images inside container have lazyloaded class
6708 function setAsLoaded() {
6709 $container.removeClass('loading').addClass('loaded');
6710 }
6711
6712 function checkForLazyloadedImage() {
6713 return $container.find('.lazyloaded').length;
6714 }
6715
6716 // If it has SVGs it's in the onboarding state so set as loaded
6717 if ($container.find('svg').length) {
6718 setAsLoaded();
6719 return;
6720 };
6721
6722 if (checkForLazyloadedImage() > 0) {
6723 setAsLoaded();
6724 return;
6725 }
6726
6727 var interval = setInterval(function() {
6728 if (checkForLazyloadedImage() > 0) {
6729 clearInterval(interval);
6730 setAsLoaded();
6731 }
6732 }, 80);
6733 }
6734
6735 theme.isSessionStorageSupported = function() {
6736 // Return false if we are in an iframe without access to sessionStorage
6737 if (window.self !== window.top) {
6738 return false;
6739 }
6740
6741 var testKey = 'test';
6742 var storage = window.sessionStorage;
6743 try {
6744 storage.setItem(testKey, '1');
6745 storage.removeItem(testKey);
6746 return true;
6747 } catch (error) {
6748 return false;
6749 }
6750 };
6751
6752 theme.isElementVisible = function($el, threshold) {
6753 var rect = $el[0].getBoundingClientRect();
6754 var windowHeight = window.innerHeight || document.documentElement.clientHeight;
6755 threshold = threshold ? threshold : 0;
6756
6757 return (
6758 rect.bottom >= (0 - (threshold / 1.5)) &&
6759 rect.right >= 0 &&
6760 rect.top <= (windowHeight + threshold) &&
6761 rect.left <= (window.innerWidth || document.documentElement.clientWidth)
6762 );
6763 };
6764
6765 theme.pageTransitions = function() {
6766 if ($('body').data('transitions') == true) {
6767
6768 // Hack test to fix Safari page cache issue.
6769 // window.onpageshow doesn't always run when navigating
6770 // back to the page, so the unloading class remains, leaving
6771 // a white page. Setting a timeout to remove that class when leaving
6772 // the page actually finishes running when they come back.
6773 if (theme.config.isSafari) {
6774 $('a').on('click', function() {
6775 window.setTimeout(function() {
6776 $('body').removeClass('unloading');
6777 }, 1200);
6778 });
6779 }
6780
6781 // Add disable transition class to malito, anchor, and YouTube links
6782 $('a[href^="mailto:"], a[href^="#"], a[target="_blank"], a[href*="youtube.com/watch"], a[href*="youtu.be/"]').each(function() {
6783 $(this).addClass('js-no-transition');
6784 });
6785
6786 $('a:not(.js-no-transition)').on('click', function(evt) {
6787 if (evt.metaKey) return true;
6788
6789 var src = $(this).attr('href');
6790
6791 // Bail if it's a hash link
6792 if(src.indexOf(location.pathname) >= 0 && src.indexOf('#') >= 0) {
6793 return true;
6794 }
6795
6796 evt.preventDefault();
6797 $('body').addClass('unloading');
6798 window.setTimeout(function() {
6799 location.href = src;
6800 }, 50);
6801 });
6802 }
6803 };
6804
6805 window.onpageshow = function(evt) {
6806 // Removes unload class when returning to page via history
6807 if (evt.persisted) {
6808 $('body').removeClass('unloading');
6809 }
6810 };
6811
6812 theme.initSecondary = function() {
6813 document.body.classList.add('js-animate');
6814 AOS.init({
6815 easing: 'ease-out-quad',
6816 once: false,
6817 mirror: true,
6818 offset: 100,
6819 disableMutationObserver: true
6820 });
6821
6822 document.addEventListener('lazyloaded', function(evt) {
6823 var $img = $(evt.target);
6824 if ($img.length) {
6825 $img.parent().addClass('loaded');
6826 }
6827 });
6828
6829 document.dispatchEvent(new CustomEvent('page:loaded'));
6830
6831 theme.reviewAppLinkListener();
6832 theme.checkForAnchorLink();
6833 };
6834
6835 theme.reviewAppLinkListener = function() {
6836 $('body').on('click', '.spr-pagination', function() {
6837 var $scroller = $(this).closest('.spr-reviews').scrollLeft(0);
6838 });
6839 };
6840
6841 theme.checkForAnchorLink = function() {
6842 if(window.location.hash) {
6843 var $el = $(window.location.hash);
6844 if ($el.length) {
6845 var top = $el.offset().top - 100;
6846 if (top > 0) {
6847 window.scroll(0, top);
6848 }
6849 }
6850 }
6851 };
6852
6853 theme.reinitProductGridItem = function($scope) {
6854 if (AOS) {
6855 AOS.refreshHard();
6856 }
6857
6858 // Refresh currency
6859 if (theme.settings.currenciesEnabled) {
6860 theme.currencySwitcher.ajaxrefresh();
6861 }
6862
6863 // Refresh reviews app
6864 if (window.SPR) {
6865 SPR.initDomEls();SPR.loadBadges();
6866 }
6867
6868 // Re-register product templates in quick view modals.
6869 // Will not double-register.
6870 // theme.initQuickShop(true);
6871 // sections.register('product-template', theme.Product, $scope);
6872
6873 // Re-hook up collapsible box triggers
6874 theme.collapsibles.init();
6875 };
6876
6877 $(document).ready(function() {
6878 theme.init();
6879
6880 // Init CSS-dependent scripts
6881 slate.utils.promiseStylesheet().then(function() {
6882 theme.initSecondary();
6883 });
6884
6885 window.sections = new theme.Sections();
6886 sections.register('header-section', theme.HeaderSection);
6887 sections.register('slideshow-section', theme.SlideshowSection);
6888 sections.register('hero-animated', theme.HeroAnimated);
6889 sections.register('video-section', theme.VideoSection);
6890 sections.register('product', theme.Product);
6891 sections.register('product-recommendations', theme.Recommendations);
6892 sections.register('product-template', theme.Product);
6893 sections.register('featured-collection', theme.FeaturedCollection);
6894 sections.register('collection-template', theme.Collection);
6895 sections.register('featured-content-section', theme.FeaturedContentSection);
6896 sections.register('testimonials', theme.Testimonials);
6897 sections.register('newsletter-popup', theme.NewsletterPopup);
6898 sections.register('map', theme.Maps);
6899 sections.register('blog', theme.Blog);
6900 });
6901
6902})(theme.jQuery);