· 6 years ago · Oct 15, 2019, 07:47 PM
1window.theme = window.theme || {};
2
3/* ================ SLATE ================ */
4window.theme = window.theme || {};
5
6theme.Sections = function Sections() {
7 this.constructors = {};
8 this.instances = [];
9
10 $(document)
11 .on('shopify:section:load', this._onSectionLoad.bind(this))
12 .on('shopify:section:unload', this._onSectionUnload.bind(this))
13 .on('shopify:section:select', this._onSelect.bind(this))
14 .on('shopify:section:deselect', this._onDeselect.bind(this))
15 .on('shopify:block:select', this._onBlockSelect.bind(this))
16 .on('shopify:block:deselect', this._onBlockDeselect.bind(this));
17};
18
19theme.Sections.prototype = _.assignIn({}, theme.Sections.prototype, {
20 _createInstance: function(container, constructor) {
21 var $container = $(container);
22 var id = $container.attr('data-section-id');
23 var type = $container.attr('data-section-type');
24
25 constructor = constructor || this.constructors[type];
26
27 if (_.isUndefined(constructor)) {
28 return;
29 }
30
31 var instance = _.assignIn(new constructor(container), {
32 id: id,
33 type: type,
34 container: container
35 });
36
37 this.instances.push(instance);
38 },
39
40 _onSectionLoad: function(evt) {
41 var container = $('[data-section-id]', evt.target)[0];
42 if (container) {
43 this._createInstance(container);
44 }
45 },
46
47 _onSectionUnload: function(evt) {
48 this.instances = _.filter(this.instances, function(instance) {
49 var isEventInstance = instance.id === evt.detail.sectionId;
50
51 if (isEventInstance) {
52 if (_.isFunction(instance.onUnload)) {
53 instance.onUnload(evt);
54 }
55 }
56
57 return !isEventInstance;
58 });
59 },
60
61 _onSelect: function(evt) {
62 // eslint-disable-next-line no-shadow
63 var instance = _.find(this.instances, function(instance) {
64 return instance.id === evt.detail.sectionId;
65 });
66
67 if (!_.isUndefined(instance) && _.isFunction(instance.onSelect)) {
68 instance.onSelect(evt);
69 }
70 },
71
72 _onDeselect: function(evt) {
73 // eslint-disable-next-line no-shadow
74 var instance = _.find(this.instances, function(instance) {
75 return instance.id === evt.detail.sectionId;
76 });
77
78 if (!_.isUndefined(instance) && _.isFunction(instance.onDeselect)) {
79 instance.onDeselect(evt);
80 }
81 },
82
83 _onBlockSelect: function(evt) {
84 // eslint-disable-next-line no-shadow
85 var instance = _.find(this.instances, function(instance) {
86 return instance.id === evt.detail.sectionId;
87 });
88
89 if (!_.isUndefined(instance) && _.isFunction(instance.onBlockSelect)) {
90 instance.onBlockSelect(evt);
91 }
92 },
93
94 _onBlockDeselect: function(evt) {
95 // eslint-disable-next-line no-shadow
96 var instance = _.find(this.instances, function(instance) {
97 return instance.id === evt.detail.sectionId;
98 });
99
100 if (!_.isUndefined(instance) && _.isFunction(instance.onBlockDeselect)) {
101 instance.onBlockDeselect(evt);
102 }
103 },
104
105 register: function(type, constructor) {
106 this.constructors[type] = constructor;
107
108 $('[data-section-type=' + type + ']').each(
109 function(index, container) {
110 this._createInstance(container, constructor);
111 }.bind(this)
112 );
113 }
114});
115
116window.slate = window.slate || {};
117
118/**
119 * Slate utilities
120 * -----------------------------------------------------------------------------
121 * A collection of useful utilities to help build your theme
122 *
123 *
124 * @namespace utils
125 */
126
127slate.utils = {
128 /**
129 * Get the query params in a Url
130 * Ex
131 * https://mysite.com/search?q=noodles&b
132 * getParameterByName('q') = "noodles"
133 * getParameterByName('b') = "" (empty value)
134 * getParameterByName('test') = null (absent)
135 */
136 getParameterByName: function(name, url) {
137 if (!url) url = window.location.href;
138 name = name.replace(/[[\]]/g, '\\$&');
139 var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
140 results = regex.exec(url);
141 if (!results) return null;
142 if (!results[2]) return '';
143 return decodeURIComponent(results[2].replace(/\+/g, ' '));
144 },
145
146 keyboardKeys: {
147 TAB: 9,
148 ENTER: 13,
149 ESCAPE: 27,
150 LEFTARROW: 37,
151 RIGHTARROW: 39
152 }
153};
154
155window.slate = window.slate || {};
156
157/**
158 * iFrames
159 * -----------------------------------------------------------------------------
160 * Wrap videos in div to force responsive layout.
161 *
162 * @namespace iframes
163 */
164
165slate.rte = {
166 /**
167 * Wrap tables in a container div to make them scrollable when needed
168 *
169 * @param {object} options - Options to be used
170 * @param {jquery} options.$tables - jquery object(s) of the table(s) to wrap
171 * @param {string} options.tableWrapperClass - table wrapper class name
172 */
173 wrapTable: function(options) {
174 options.$tables.wrap(
175 '<div class="' + options.tableWrapperClass + '"></div>'
176 );
177 },
178
179 /**
180 * Wrap iframes in a container div to make them responsive
181 *
182 * @param {object} options - Options to be used
183 * @param {jquery} options.$iframes - jquery object(s) of the iframe(s) to wrap
184 * @param {string} options.iframeWrapperClass - class name used on the wrapping div
185 */
186 wrapIframe: function(options) {
187 options.$iframes.each(function() {
188 // Add wrapper to make video responsive
189 $(this).wrap('<div class="' + options.iframeWrapperClass + '"></div>');
190
191 // Re-set the src attribute on each iframe after page load
192 // for Chrome's "incorrect iFrame content on 'back'" bug.
193 // https://code.google.com/p/chromium/issues/detail?id=395791
194 // Need to specifically target video and admin bar
195 this.src = this.src;
196 });
197 }
198};
199
200window.slate = window.slate || {};
201
202/**
203 * A11y Helpers
204 * -----------------------------------------------------------------------------
205 * A collection of useful functions that help make your theme more accessible
206 * to users with visual impairments.
207 *
208 *
209 * @namespace a11y
210 */
211
212slate.a11y = {
213 /**
214 * For use when focus shifts to a container rather than a link
215 * eg for In-page links, after scroll, focus shifts to content area so that
216 * next `tab` is where user expects if focusing a link, just $link.focus();
217 *
218 * @param {JQuery} $element - The element to be acted upon
219 */
220 pageLinkFocus: function($element) {
221 var focusClass = 'js-focus-hidden';
222
223 $element
224 .first()
225 .attr('tabIndex', '-1')
226 .focus()
227 .addClass(focusClass)
228 .one('blur', callback);
229
230 function callback() {
231 $element
232 .first()
233 .removeClass(focusClass)
234 .removeAttr('tabindex');
235 }
236 },
237
238 /**
239 * If there's a hash in the url, focus the appropriate element
240 */
241 focusHash: function() {
242 var hash = window.location.hash;
243
244 // is there a hash in the url? is it an element on the page?
245 if (hash && document.getElementById(hash.slice(1))) {
246 this.pageLinkFocus($(hash));
247 }
248 },
249
250 /**
251 * When an in-page (url w/hash) link is clicked, focus the appropriate element
252 */
253 bindInPageLinks: function() {
254 $('a[href*=#]').on(
255 'click',
256 function(evt) {
257 this.pageLinkFocus($(evt.currentTarget.hash));
258 }.bind(this)
259 );
260 },
261
262 /**
263 * Traps the focus in a particular container
264 *
265 * @param {object} options - Options to be used
266 * @param {jQuery} options.$container - Container to trap focus within
267 * @param {jQuery} options.$elementToFocus - Element to be focused when focus leaves container
268 * @param {string} options.namespace - Namespace used for new focus event handler
269 */
270 trapFocus: function(options) {
271 var eventsName = {
272 focusin: options.namespace ? 'focusin.' + options.namespace : 'focusin',
273 focusout: options.namespace
274 ? 'focusout.' + options.namespace
275 : 'focusout',
276 keydown: options.namespace
277 ? 'keydown.' + options.namespace
278 : 'keydown.handleFocus'
279 };
280
281 /**
282 * Get every possible visible focusable element
283 */
284 var $focusableElements = options.$container.find(
285 $(
286 'button, [href], input, select, textarea, [tabindex]:not([tabindex^="-"])'
287 ).filter(':visible')
288 );
289 var firstFocusable = $focusableElements[0];
290 var lastFocusable = $focusableElements[$focusableElements.length - 1];
291
292 if (!options.$elementToFocus) {
293 options.$elementToFocus = options.$container;
294 }
295
296 function _manageFocus(evt) {
297 if (evt.keyCode !== slate.utils.keyboardKeys.TAB) return;
298
299 /**
300 * On the last focusable element and tab forward,
301 * focus the first element.
302 */
303 if (evt.target === lastFocusable && !evt.shiftKey) {
304 evt.preventDefault();
305 firstFocusable.focus();
306 }
307 /**
308 * On the first focusable element and tab backward,
309 * focus the last element.
310 */
311 if (evt.target === firstFocusable && evt.shiftKey) {
312 evt.preventDefault();
313 lastFocusable.focus();
314 }
315 }
316
317 options.$container.attr('tabindex', '-1');
318 options.$elementToFocus.focus();
319
320 $(document).off('focusin');
321
322 $(document).on(eventsName.focusout, function() {
323 $(document).off(eventsName.keydown);
324 });
325
326 $(document).on(eventsName.focusin, function(evt) {
327 if (evt.target !== lastFocusable && evt.target !== firstFocusable) return;
328
329 $(document).on(eventsName.keydown, function(evt) {
330 _manageFocus(evt);
331 });
332 });
333 },
334
335 /**
336 * Removes the trap of focus in a particular container
337 *
338 * @param {object} options - Options to be used
339 * @param {jQuery} options.$container - Container to trap focus within
340 * @param {string} options.namespace - Namespace used for new focus event handler
341 */
342 removeTrapFocus: function(options) {
343 var eventName = options.namespace
344 ? 'focusin.' + options.namespace
345 : 'focusin';
346
347 if (options.$container && options.$container.length) {
348 options.$container.removeAttr('tabindex');
349 }
350
351 $(document).off(eventName);
352 },
353
354 /**
355 * Add aria-describedby attribute to external and new window links
356 *
357 * @param {object} options - Options to be used
358 * @param {object} options.messages - Custom messages to be used
359 * @param {jQuery} options.$links - Specific links to be targeted
360 */
361 accessibleLinks: function(options) {
362 var body = document.querySelector('body');
363
364 var idSelectors = {
365 newWindow: 'a11y-new-window-message',
366 external: 'a11y-external-message',
367 newWindowExternal: 'a11y-new-window-external-message'
368 };
369
370 if (options.$links === undefined || !options.$links.jquery) {
371 options.$links = $('a[href]:not([aria-describedby])');
372 }
373
374 function generateHTML(customMessages) {
375 if (typeof customMessages !== 'object') {
376 customMessages = {};
377 }
378
379 var messages = $.extend(
380 {
381 newWindow: 'Opens in a new window.',
382 external: 'Opens external website.',
383 newWindowExternal: 'Opens external website in a new window.'
384 },
385 customMessages
386 );
387
388 var container = document.createElement('ul');
389 var htmlMessages = '';
390
391 for (var message in messages) {
392 htmlMessages +=
393 '<li id=' + idSelectors[message] + '>' + messages[message] + '</li>';
394 }
395
396 container.setAttribute('hidden', true);
397 container.innerHTML = htmlMessages;
398
399 body.appendChild(container);
400 }
401
402 function _externalSite($link) {
403 var hostname = window.location.hostname;
404
405 return $link[0].hostname !== hostname;
406 }
407
408 $.each(options.$links, function() {
409 var $link = $(this);
410 var target = $link.attr('target');
411 var rel = $link.attr('rel');
412 var isExternal = _externalSite($link);
413 var isTargetBlank = target === '_blank';
414
415 if (isExternal) {
416 $link.attr('aria-describedby', idSelectors.external);
417 }
418 if (isTargetBlank) {
419 if (rel === undefined || rel.indexOf('noopener') === -1) {
420 $link.attr('rel', function(i, val) {
421 var relValue = val === undefined ? '' : val + ' ';
422 return relValue + 'noopener';
423 });
424 }
425 $link.attr('aria-describedby', idSelectors.newWindow);
426 }
427 if (isExternal && isTargetBlank) {
428 $link.attr('aria-describedby', idSelectors.newWindowExternal);
429 }
430 });
431
432 generateHTML(options.messages);
433 }
434};
435
436/**
437 * Image Helper Functions
438 * -----------------------------------------------------------------------------
439 * A collection of functions that help with basic image operations.
440 *
441 */
442
443theme.Images = (function() {
444 /**
445 * Preloads an image in memory and uses the browsers cache to store it until needed.
446 *
447 * @param {Array} images - A list of image urls
448 * @param {String} size - A shopify image size attribute
449 */
450
451 function preload(images, size) {
452 if (typeof images === 'string') {
453 images = [images];
454 }
455
456 for (var i = 0; i < images.length; i++) {
457 var image = images[i];
458 this.loadImage(this.getSizedImageUrl(image, size));
459 }
460 }
461
462 /**
463 * Loads and caches an image in the browsers cache.
464 * @param {string} path - An image url
465 */
466 function loadImage(path) {
467 new Image().src = path;
468 }
469
470 /**
471 * Swaps the src of an image for another OR returns the imageURL to the callback function
472 * @param image
473 * @param element
474 * @param callback
475 */
476 function switchImage(image, element, callback) {
477 var size = this.imageSize(element.src);
478 var imageUrl = this.getSizedImageUrl(image.src, size);
479
480 if (callback) {
481 callback(imageUrl, image, element); // eslint-disable-line callback-return
482 } else {
483 element.src = imageUrl;
484 }
485 }
486
487 /**
488 * +++ Useful
489 * Find the Shopify image attribute size
490 *
491 * @param {string} src
492 * @returns {null}
493 */
494 function imageSize(src) {
495 var match = src.match(
496 /.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\\.@]/
497 );
498
499 if (match !== null) {
500 if (match[2] !== undefined) {
501 return match[1] + match[2];
502 } else {
503 return match[1];
504 }
505 } else {
506 return null;
507 }
508 }
509
510 /**
511 * +++ Useful
512 * Adds a Shopify size attribute to a URL
513 *
514 * @param src
515 * @param size
516 * @returns {*}
517 */
518 function getSizedImageUrl(src, size) {
519 if (size === null) {
520 return src;
521 }
522
523 if (size === 'master') {
524 return this.removeProtocol(src);
525 }
526
527 var match = src.match(
528 /\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i
529 );
530
531 if (match !== null) {
532 var prefix = src.split(match[0]);
533 var suffix = match[0];
534
535 return this.removeProtocol(prefix[0] + '_' + size + suffix);
536 }
537
538 return null;
539 }
540
541 function removeProtocol(path) {
542 return path.replace(/http(s)?:/, '');
543 }
544
545 return {
546 preload: preload,
547 loadImage: loadImage,
548 switchImage: switchImage,
549 imageSize: imageSize,
550 getSizedImageUrl: getSizedImageUrl,
551 removeProtocol: removeProtocol
552 };
553})();
554
555/**
556 * Currency Helpers
557 * -----------------------------------------------------------------------------
558 * A collection of useful functions that help with currency formatting
559 *
560 * Current contents
561 * - formatMoney - Takes an amount in cents and returns it as a formatted dollar value.
562 *
563 * Alternatives
564 * - Accounting.js - http://openexchangerates.github.io/accounting.js/
565 *
566 */
567
568theme.Currency = (function() {
569 var moneyFormat = '${{amount}}'; // eslint-disable-line camelcase
570
571 function formatMoney(cents, format) {
572 if (typeof cents === 'string') {
573 cents = cents.replace('.', '');
574 }
575 var value = '';
576 var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/;
577 var formatString = format || moneyFormat;
578
579 function formatWithDelimiters(number, precision, thousands, decimal) {
580 thousands = thousands || ',';
581 decimal = decimal || '.';
582
583 if (isNaN(number) || number === null) {
584 return 0;
585 }
586
587 number = (number / 100.0).toFixed(precision);
588
589 var parts = number.split('.');
590 var dollarsAmount = parts[0].replace(
591 /(\d)(?=(\d\d\d)+(?!\d))/g,
592 '$1' + thousands
593 );
594 var centsAmount = parts[1] ? decimal + parts[1] : '';
595
596 return dollarsAmount + centsAmount;
597 }
598
599 switch (formatString.match(placeholderRegex)[1]) {
600 case 'amount':
601 value = formatWithDelimiters(cents, 2);
602 break;
603 case 'amount_no_decimals':
604 value = formatWithDelimiters(cents, 0);
605 break;
606 case 'amount_with_comma_separator':
607 value = formatWithDelimiters(cents, 2, '.', ',');
608 break;
609 case 'amount_no_decimals_with_comma_separator':
610 value = formatWithDelimiters(cents, 0, '.', ',');
611 break;
612 case 'amount_no_decimals_with_space_separator':
613 value = formatWithDelimiters(cents, 0, ' ');
614 break;
615 case 'amount_with_apostrophe_separator':
616 value = formatWithDelimiters(cents, 2, "'");
617 break;
618 }
619
620 return formatString.replace(placeholderRegex, value);
621 }
622
623 return {
624 formatMoney: formatMoney
625 };
626})();
627
628/**
629 * Variant Selection scripts
630 * ------------------------------------------------------------------------------
631 *
632 * Handles change events from the variant inputs in any `cart/add` forms that may
633 * exist. Also updates the master select and triggers updates when the variants
634 * price or image changes.
635 *
636 * @namespace variants
637 */
638
639slate.Variants = (function() {
640 /**
641 * Variant constructor
642 *
643 * @param {object} options - Settings from `product.js`
644 */
645 function Variants(options) {
646 this.$container = options.$container;
647 this.product = options.product;
648 this.singleOptionSelector = options.singleOptionSelector;
649 this.originalSelectorId = options.originalSelectorId;
650 this.enableHistoryState = options.enableHistoryState;
651 this.currentVariant = this._getVariantFromOptions();
652
653 $(this.singleOptionSelector, this.$container).on(
654 'change',
655 this._onSelectChange.bind(this)
656 );
657 }
658
659 Variants.prototype = _.assignIn({}, Variants.prototype, {
660 /**
661 * Get the currently selected options from add-to-cart form. Works with all
662 * form input elements.
663 *
664 * @return {array} options - Values of currently selected variants
665 */
666 _getCurrentOptions: function() {
667 var currentOptions = _.map(
668 $(this.singleOptionSelector, this.$container),
669 function(element) {
670 var $element = $(element);
671 var type = $element.attr('type');
672 var currentOption = {};
673
674 if (type === 'radio' || type === 'checkbox') {
675 if ($element[0].checked) {
676 currentOption.value = $element.val();
677 currentOption.index = $element.data('index');
678
679 return currentOption;
680 } else {
681 return false;
682 }
683 } else {
684 currentOption.value = $element.val();
685 currentOption.index = $element.data('index');
686
687 return currentOption;
688 }
689 }
690 );
691
692 // remove any unchecked input values if using radio buttons or checkboxes
693 currentOptions = _.compact(currentOptions);
694
695 return currentOptions;
696 },
697
698 /**
699 * Find variant based on selected values.
700 *
701 * @param {array} selectedValues - Values of variant inputs
702 * @return {object || undefined} found - Variant object from product.variants
703 */
704 _getVariantFromOptions: function() {
705 var selectedValues = this._getCurrentOptions();
706 var variants = this.product.variants;
707
708 var found = _.find(variants, function(variant) {
709 return selectedValues.every(function(values) {
710 return _.isEqual(variant[values.index], values.value);
711 });
712 });
713
714 return found;
715 },
716
717 /**
718 * Event handler for when a variant input changes.
719 */
720 _onSelectChange: function() {
721 var variant = this._getVariantFromOptions();
722
723 this.$container.trigger({
724 type: 'variantChange',
725 variant: variant
726 });
727
728 if (!variant) {
729 return;
730 }
731
732 this._updateMasterSelect(variant);
733 this._updateImages(variant);
734 this._updatePrice(variant);
735 this._updateSKU(variant);
736 this.currentVariant = variant;
737
738 if (this.enableHistoryState) {
739 this._updateHistoryState(variant);
740 }
741 },
742
743 /**
744 * Trigger event when variant image changes
745 *
746 * @param {object} variant - Currently selected variant
747 * @return {event} variantImageChange
748 */
749 _updateImages: function(variant) {
750 var variantImage = variant.featured_image || {};
751 var currentVariantImage = this.currentVariant.featured_image || {};
752
753 if (
754 !variant.featured_image ||
755 variantImage.src === currentVariantImage.src
756 ) {
757 return;
758 }
759
760 this.$container.trigger({
761 type: 'variantImageChange',
762 variant: variant
763 });
764 },
765
766 /**
767 * Trigger event when variant price changes.
768 *
769 * @param {object} variant - Currently selected variant
770 * @return {event} variantPriceChange
771 */
772 _updatePrice: function(variant) {
773 if (
774 variant.price === this.currentVariant.price &&
775 variant.compare_at_price === this.currentVariant.compare_at_price
776 ) {
777 return;
778 }
779
780 this.$container.trigger({
781 type: 'variantPriceChange',
782 variant: variant
783 });
784 },
785
786 /**
787 * Trigger event when variant sku changes.
788 *
789 * @param {object} variant - Currently selected variant
790 * @return {event} variantSKUChange
791 */
792 _updateSKU: function(variant) {
793 if (variant.sku === this.currentVariant.sku) {
794 return;
795 }
796
797 this.$container.trigger({
798 type: 'variantSKUChange',
799 variant: variant
800 });
801 },
802
803 /**
804 * Update history state for product deeplinking
805 *
806 * @param {variant} variant - Currently selected variant
807 * @return {k} [description]
808 */
809 _updateHistoryState: function(variant) {
810 if (!history.replaceState || !variant) {
811 return;
812 }
813
814 var newurl =
815 window.location.protocol +
816 '//' +
817 window.location.host +
818 window.location.pathname +
819 '?variant=' +
820 variant.id;
821 window.history.replaceState({ path: newurl }, '', newurl);
822 },
823
824 /**
825 * Update hidden master select of variant change
826 *
827 * @param {variant} variant - Currently selected variant
828 */
829 _updateMasterSelect: function(variant) {
830 $(this.originalSelectorId, this.$container).val(variant.id);
831 }
832 });
833
834 return Variants;
835})();
836
837
838/* ================ GLOBAL ================ */
839/*============================================================================
840 Drawer modules
841==============================================================================*/
842theme.Drawers = (function() {
843 function Drawer(id, position, options) {
844 var defaults = {
845 close: '.js-drawer-close',
846 open: '.js-drawer-open-' + position,
847 openClass: 'js-drawer-open',
848 dirOpenClass: 'js-drawer-open-' + position
849 };
850
851 this.nodes = {
852 $parent: $('html').add('body'),
853 $page: $('#PageContainer')
854 };
855
856 this.config = $.extend(defaults, options);
857 this.position = position;
858
859 this.$drawer = $('#' + id);
860
861 if (!this.$drawer.length) {
862 return false;
863 }
864
865 this.drawerIsOpen = false;
866 this.init();
867 }
868
869 Drawer.prototype.init = function() {
870 $(this.config.open).on('click', $.proxy(this.open, this));
871 this.$drawer.on('click', this.config.close, $.proxy(this.close, this));
872 };
873
874 Drawer.prototype.open = function(evt) {
875 // Keep track if drawer was opened from a click, or called by another function
876 var externalCall = false;
877
878 // Prevent following href if link is clicked
879 if (evt) {
880 evt.preventDefault();
881 } else {
882 externalCall = true;
883 }
884
885 // Without this, the drawer opens, the click event bubbles up to nodes.$page
886 // which closes the drawer.
887 if (evt && evt.stopPropagation) {
888 evt.stopPropagation();
889 // save the source of the click, we'll focus to this on close
890 this.$activeSource = $(evt.currentTarget);
891 }
892
893 if (this.drawerIsOpen && !externalCall) {
894 return this.close();
895 }
896
897 // Add is-transitioning class to moved elements on open so drawer can have
898 // transition for close animation
899 this.$drawer.prepareTransition();
900
901 this.nodes.$parent.addClass(
902 this.config.openClass + ' ' + this.config.dirOpenClass
903 );
904 this.drawerIsOpen = true;
905
906 // Set focus on drawer
907 slate.a11y.trapFocus({
908 $container: this.$drawer,
909 namespace: 'drawer_focus'
910 });
911
912 // Run function when draw opens if set
913 if (
914 this.config.onDrawerOpen &&
915 typeof this.config.onDrawerOpen === 'function'
916 ) {
917 if (!externalCall) {
918 this.config.onDrawerOpen();
919 }
920 }
921
922 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
923 this.$activeSource.attr('aria-expanded', 'true');
924 }
925
926 this.bindEvents();
927
928 return this;
929 };
930
931 Drawer.prototype.close = function() {
932 if (!this.drawerIsOpen) {
933 // don't close a closed drawer
934 return;
935 }
936
937 // deselect any focused form elements
938 $(document.activeElement).trigger('blur');
939
940 // Ensure closing transition is applied to moved elements, like the nav
941 this.$drawer.prepareTransition();
942
943 this.nodes.$parent.removeClass(
944 this.config.dirOpenClass + ' ' + this.config.openClass
945 );
946
947 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
948 this.$activeSource.attr('aria-expanded', 'false');
949 }
950
951 this.drawerIsOpen = false;
952
953 // Remove focus on drawer
954 slate.a11y.removeTrapFocus({
955 $container: this.$drawer,
956 namespace: 'drawer_focus'
957 });
958
959 this.unbindEvents();
960
961 // Run function when draw closes if set
962 if (
963 this.config.onDrawerClose &&
964 typeof this.config.onDrawerClose === 'function'
965 ) {
966 this.config.onDrawerClose();
967 }
968 };
969
970 Drawer.prototype.bindEvents = function() {
971 this.nodes.$parent.on(
972 'keyup.drawer',
973 $.proxy(function(evt) {
974 // close on 'esc' keypress
975 if (evt.keyCode === 27) {
976 this.close();
977 return false;
978 } else {
979 return true;
980 }
981 }, this)
982 );
983
984 // Lock scrolling on mobile
985 this.nodes.$page.on('touchmove.drawer', function() {
986 return false;
987 });
988
989 this.nodes.$page.on(
990 'click.drawer',
991 $.proxy(function() {
992 this.close();
993 return false;
994 }, this)
995 );
996 };
997
998 Drawer.prototype.unbindEvents = function() {
999 this.nodes.$page.off('.drawer');
1000 this.nodes.$parent.off('.drawer');
1001 };
1002
1003 return Drawer;
1004})();
1005
1006
1007/* ================ MODULES ================ */
1008window.theme = window.theme || {};
1009
1010theme.Header = (function() {
1011 var selectors = {
1012 body: 'body',
1013 multicurrencySelector: '[data-currency-selector]',
1014 navigation: '#AccessibleNav',
1015 siteNavHasDropdown: '[data-has-dropdowns]',
1016 siteNavChildLinks: '.site-nav__child-link',
1017 siteNavActiveDropdown: '.site-nav--active-dropdown',
1018 siteNavHasCenteredDropdown: '.site-nav--has-centered-dropdown',
1019 siteNavCenteredDropdown: '.site-nav__dropdown--centered',
1020 siteNavLinkMain: '.site-nav__link--main',
1021 siteNavChildLink: '.site-nav__link--last',
1022 siteNavDropdown: '.site-nav__dropdown',
1023 siteHeader: '.site-header'
1024 };
1025
1026 var config = {
1027 activeClass: 'site-nav--active-dropdown',
1028 childLinkClass: 'site-nav__child-link',
1029 rightDropdownClass: 'site-nav__dropdown--right',
1030 leftDropdownClass: 'site-nav__dropdown--left'
1031 };
1032
1033 var cache = {};
1034
1035 function init() {
1036 cacheSelectors();
1037 styleDropdowns($(selectors.siteNavHasDropdown));
1038 positionFullWidthDropdowns();
1039
1040 cache.$parents.on('click.siteNav', function() {
1041 var $el = $(this);
1042 $el.hasClass(config.activeClass) ? hideDropdown($el) : showDropdown($el);
1043 });
1044
1045 // check when we're leaving a dropdown and close the active dropdown
1046 $(selectors.siteNavChildLink).on('focusout.siteNav', function() {
1047 setTimeout(function() {
1048 if (
1049 $(document.activeElement).hasClass(config.childLinkClass) ||
1050 !cache.$activeDropdown.length
1051 ) {
1052 return;
1053 }
1054
1055 hideDropdown(cache.$activeDropdown);
1056 });
1057 });
1058
1059 // close dropdowns when on top level nav
1060 cache.$topLevel.on('focus.siteNav', function() {
1061 if (cache.$activeDropdown.length) {
1062 hideDropdown(cache.$activeDropdown);
1063 }
1064 });
1065
1066 cache.$subMenuLinks.on('click.siteNav', function(evt) {
1067 // Prevent click on body from firing instead of link
1068 evt.stopImmediatePropagation();
1069 });
1070
1071 $(selectors.multicurrencySelector).on('change', function() {
1072 $(this)
1073 .parents('form')
1074 .submit();
1075 });
1076
1077 $(window).resize(
1078 $.debounce(50, function() {
1079 styleDropdowns($(selectors.siteNavHasDropdown));
1080 positionFullWidthDropdowns();
1081 })
1082 );
1083 }
1084
1085 function cacheSelectors() {
1086 cache = {
1087 $nav: $(selectors.navigation),
1088 $topLevel: $(selectors.siteNavLinkMain),
1089 $parents: $(selectors.navigation).find(selectors.siteNavHasDropdown),
1090 $subMenuLinks: $(selectors.siteNavChildLinks),
1091 $activeDropdown: $(selectors.siteNavActiveDropdown),
1092 $siteHeader: $(selectors.siteHeader)
1093 };
1094 }
1095
1096 function showDropdown($el) {
1097 $el.addClass(config.activeClass);
1098
1099 // close open dropdowns
1100 if (cache.$activeDropdown.length) {
1101 hideDropdown(cache.$activeDropdown);
1102 }
1103
1104 cache.$activeDropdown = $el;
1105
1106 // set expanded on open dropdown
1107 $el.find(selectors.siteNavLinkMain).attr('aria-expanded', 'true');
1108
1109 setTimeout(function() {
1110 $(window).on('keyup.siteNav', function(evt) {
1111 if (evt.keyCode === 27) {
1112 hideDropdown($el);
1113 }
1114 });
1115
1116 $(selectors.body).on('click.siteNav', function() {
1117 hideDropdown($el);
1118 });
1119 }, 250);
1120 }
1121
1122 function hideDropdown($el) {
1123 // remove aria on open dropdown
1124 $el.find(selectors.siteNavLinkMain).attr('aria-expanded', 'false');
1125 $el.removeClass(config.activeClass);
1126
1127 // reset active dropdown
1128 cache.$activeDropdown = $(selectors.siteNavActiveDropdown);
1129
1130 $(selectors.body).off('click.siteNav');
1131 $(window).off('keyup.siteNav');
1132 }
1133
1134 function styleDropdowns($dropdownListItems) {
1135 $dropdownListItems.each(function() {
1136 var $dropdownLi = $(this).find(selectors.siteNavDropdown);
1137 if (!$dropdownLi.length) {
1138 return;
1139 }
1140 var isRightOfLogo =
1141 Math.ceil($(this).offset().left) >
1142 Math.floor(cache.$siteHeader.outerWidth()) / 2
1143 ? true
1144 : false;
1145 if (isRightOfLogo) {
1146 $dropdownLi
1147 .removeClass(config.leftDropdownClass)
1148 .addClass(config.rightDropdownClass);
1149 } else {
1150 $dropdownLi
1151 .removeClass(config.rightDropdownClass)
1152 .addClass(config.leftDropdownClass);
1153 }
1154 });
1155 }
1156
1157 function positionFullWidthDropdowns() {
1158 var $listWithCenteredDropdown = $(selectors.siteNavHasCenteredDropdown);
1159
1160 $listWithCenteredDropdown.each(function() {
1161 var $hasCenteredDropdown = $(this);
1162 var $fullWidthDropdown = $hasCenteredDropdown.find(
1163 selectors.siteNavCenteredDropdown
1164 );
1165
1166 var fullWidthDropdownOffset = $hasCenteredDropdown.position().top + 41;
1167 $fullWidthDropdown.css('top', fullWidthDropdownOffset);
1168 });
1169 }
1170
1171 function unload() {
1172 $(window).off('.siteNav');
1173 cache.$parents.off('.siteNav');
1174 cache.$subMenuLinks.off('.siteNav');
1175 cache.$topLevel.off('.siteNav');
1176 $(selectors.siteNavChildLink).off('.siteNav');
1177 $(selectors.body).off('.siteNav');
1178 }
1179
1180 return {
1181 init: init,
1182 unload: unload
1183 };
1184})();
1185
1186window.theme = window.theme || {};
1187
1188theme.MobileNav = (function() {
1189 var classes = {
1190 mobileNavOpenIcon: 'mobile-nav--open',
1191 mobileNavCloseIcon: 'mobile-nav--close',
1192 navLinkWrapper: 'mobile-nav__item',
1193 navLink: 'mobile-nav__link',
1194 subNavLink: 'mobile-nav__sublist-link',
1195 return: 'mobile-nav__return-btn',
1196 subNavActive: 'is-active',
1197 subNavClosing: 'is-closing',
1198 navOpen: 'js-menu--is-open',
1199 subNavShowing: 'sub-nav--is-open',
1200 thirdNavShowing: 'third-nav--is-open',
1201 subNavToggleBtn: 'js-toggle-submenu'
1202 };
1203 var cache = {};
1204 var isTransitioning;
1205 var $activeSubNav;
1206 var $activeTrigger;
1207 var menuLevel = 1;
1208 // Breakpoints from src/stylesheets/global/variables.scss.liquid
1209 var mediaQuerySmall = 'screen and (max-width: 749px)';
1210
1211 function init() {
1212 cacheSelectors();
1213
1214 cache.$mobileNavToggle.on('click', toggleMobileNav);
1215 cache.$subNavToggleBtn.on('click.subNav', toggleSubNav);
1216
1217 // Close mobile nav when unmatching mobile breakpoint
1218 enquire.register(mediaQuerySmall, {
1219 unmatch: function() {
1220 if (cache.$mobileNavContainer.hasClass(classes.navOpen)) {
1221 closeMobileNav();
1222 }
1223 }
1224 });
1225 }
1226
1227 function toggleMobileNav() {
1228 if (cache.$mobileNavToggle.hasClass(classes.mobileNavCloseIcon)) {
1229 closeMobileNav();
1230 } else {
1231 openMobileNav();
1232 }
1233 }
1234
1235 function cacheSelectors() {
1236 cache = {
1237 $pageContainer: $('#PageContainer'),
1238 $siteHeader: $('.site-header'),
1239 $mobileNavToggle: $('.js-mobile-nav-toggle'),
1240 $mobileNavContainer: $('.mobile-nav-wrapper'),
1241 $mobileNav: $('#MobileNav'),
1242 $sectionHeader: $('#shopify-section-header'),
1243 $subNavToggleBtn: $('.' + classes.subNavToggleBtn)
1244 };
1245 }
1246
1247 function openMobileNav() {
1248 var translateHeaderHeight = cache.$siteHeader.outerHeight();
1249
1250 cache.$mobileNavContainer.prepareTransition().addClass(classes.navOpen);
1251
1252 cache.$mobileNavContainer.css({
1253 transform: 'translateY(' + translateHeaderHeight + 'px)'
1254 });
1255
1256 cache.$pageContainer.css({
1257 transform:
1258 'translate3d(0, ' + cache.$mobileNavContainer[0].scrollHeight + 'px, 0)'
1259 });
1260
1261 slate.a11y.trapFocus({
1262 $container: cache.$sectionHeader,
1263 $elementToFocus: cache.$mobileNavToggle,
1264 namespace: 'navFocus'
1265 });
1266
1267 cache.$mobileNavToggle
1268 .addClass(classes.mobileNavCloseIcon)
1269 .removeClass(classes.mobileNavOpenIcon)
1270 .attr('aria-expanded', true);
1271
1272 // close on escape
1273 $(window).on('keyup.mobileNav', function(evt) {
1274 if (evt.which === 27) {
1275 closeMobileNav();
1276 }
1277 });
1278 }
1279
1280 function closeMobileNav() {
1281 cache.$mobileNavContainer.prepareTransition().removeClass(classes.navOpen);
1282
1283 cache.$mobileNavContainer.css({
1284 transform: 'translateY(-100%)'
1285 });
1286
1287 cache.$pageContainer.removeAttr('style');
1288
1289 slate.a11y.trapFocus({
1290 $container: $('html'),
1291 $elementToFocus: $('body')
1292 });
1293
1294 cache.$mobileNavContainer.one(
1295 'TransitionEnd.navToggle webkitTransitionEnd.navToggle transitionend.navToggle oTransitionEnd.navToggle',
1296 function() {
1297 slate.a11y.removeTrapFocus({
1298 $container: cache.$mobileNav,
1299 namespace: 'navFocus'
1300 });
1301 }
1302 );
1303
1304 cache.$mobileNavToggle
1305 .addClass(classes.mobileNavOpenIcon)
1306 .removeClass(classes.mobileNavCloseIcon)
1307 .attr('aria-expanded', false)
1308 .focus();
1309
1310 $(window).off('keyup.mobileNav');
1311
1312 scrollTo(0, 0);
1313 }
1314
1315 function toggleSubNav(evt) {
1316 if (isTransitioning) {
1317 return;
1318 }
1319
1320 var $toggleBtn = $(evt.currentTarget);
1321 var isReturn = $toggleBtn.hasClass(classes.return);
1322 isTransitioning = true;
1323
1324 if (isReturn) {
1325 // Close all subnavs by removing active class on buttons
1326 $(
1327 '.' + classes.subNavToggleBtn + '[data-level="' + (menuLevel - 1) + '"]'
1328 ).removeClass(classes.subNavActive);
1329
1330 if ($activeTrigger && $activeTrigger.length) {
1331 $activeTrigger.removeClass(classes.subNavActive);
1332 }
1333 } else {
1334 $toggleBtn.addClass(classes.subNavActive);
1335 }
1336
1337 $activeTrigger = $toggleBtn;
1338
1339 goToSubnav($toggleBtn.data('target'));
1340 }
1341
1342 function goToSubnav(target) {
1343 /*eslint-disable shopify/jquery-dollar-sign-reference */
1344
1345 var $targetMenu = target
1346 ? $('.mobile-nav__dropdown[data-parent="' + target + '"]')
1347 : cache.$mobileNav;
1348
1349 menuLevel = $targetMenu.data('level') ? $targetMenu.data('level') : 1;
1350
1351 if ($activeSubNav && $activeSubNav.length) {
1352 $activeSubNav.prepareTransition().addClass(classes.subNavClosing);
1353 }
1354
1355 $activeSubNav = $targetMenu;
1356
1357 /*eslint-enable shopify/jquery-dollar-sign-reference */
1358
1359 var translateMenuHeight = $targetMenu.outerHeight();
1360
1361 var openNavClass =
1362 menuLevel > 2 ? classes.thirdNavShowing : classes.subNavShowing;
1363
1364 cache.$mobileNavContainer
1365 .css('height', translateMenuHeight)
1366 .removeClass(classes.thirdNavShowing)
1367 .addClass(openNavClass);
1368
1369 if (!target) {
1370 // Show top level nav
1371 cache.$mobileNavContainer
1372 .removeClass(classes.thirdNavShowing)
1373 .removeClass(classes.subNavShowing);
1374 }
1375
1376 /* if going back to first subnav, focus is on whole header */
1377 var $container = menuLevel === 1 ? cache.$sectionHeader : $targetMenu;
1378
1379 var $menuTitle = $targetMenu.find('[data-menu-title=' + menuLevel + ']');
1380 var $elementToFocus = $menuTitle ? $menuTitle : $targetMenu;
1381
1382 // Focusing an item in the subnav early forces element into view and breaks the animation.
1383 cache.$mobileNavContainer.one(
1384 'TransitionEnd.subnavToggle webkitTransitionEnd.subnavToggle transitionend.subnavToggle oTransitionEnd.subnavToggle',
1385 function() {
1386 slate.a11y.trapFocus({
1387 $container: $container,
1388 $elementToFocus: $elementToFocus,
1389 namespace: 'subNavFocus'
1390 });
1391
1392 cache.$mobileNavContainer.off('.subnavToggle');
1393 isTransitioning = false;
1394 }
1395 );
1396
1397 // Match height of subnav
1398 cache.$pageContainer.css({
1399 transform: 'translateY(' + translateMenuHeight + 'px)'
1400 });
1401
1402 $activeSubNav.removeClass(classes.subNavClosing);
1403 }
1404
1405 return {
1406 init: init,
1407 closeMobileNav: closeMobileNav
1408 };
1409})(jQuery);
1410
1411window.theme = window.theme || {};
1412
1413theme.Search = (function() {
1414 var selectors = {
1415 search: '.search',
1416 searchSubmit: '.search__submit',
1417 searchInput: '.search__input',
1418
1419 siteHeader: '.site-header',
1420 siteHeaderSearchToggle: '.site-header__search-toggle',
1421 siteHeaderSearch: '.site-header__search',
1422
1423 searchDrawer: '.search-bar',
1424 searchDrawerInput: '.search-bar__input',
1425
1426 searchHeader: '.search-header',
1427 searchHeaderInput: '.search-header__input',
1428 searchHeaderSubmit: '.search-header__submit',
1429
1430 searchResultSubmit: '#SearchResultSubmit',
1431 searchResultInput: '#SearchInput',
1432 searchResultMessage: '[data-search-error-message]',
1433
1434 mobileNavWrapper: '.mobile-nav-wrapper'
1435 };
1436
1437 var classes = {
1438 focus: 'search--focus',
1439 hidden: 'hide',
1440 mobileNavIsOpen: 'js-menu--is-open',
1441 searchTemplate: 'template-search'
1442 };
1443
1444 function init() {
1445 if (!$(selectors.siteHeader).length) {
1446 return;
1447 }
1448
1449 this.$searchResultInput = $(selectors.searchResultInput);
1450 this.$searchErrorMessage = $(selectors.searchResultMessage);
1451
1452 initDrawer();
1453
1454 var isSearchPage =
1455 slate.utils.getParameterByName('q') !== null &&
1456 $('body').hasClass(classes.searchTemplate);
1457
1458 if (isSearchPage) {
1459 validateSearchResultForm.call(this);
1460 }
1461
1462 $(selectors.searchResultSubmit).on(
1463 'click',
1464 validateSearchResultForm.bind(this)
1465 );
1466
1467 $(selectors.searchHeaderInput)
1468 .add(selectors.searchHeaderSubmit)
1469 .on('focus blur', function() {
1470 $(selectors.searchHeader).toggleClass(classes.focus);
1471 });
1472
1473 $(selectors.siteHeaderSearchToggle).on('click', function() {
1474 var searchHeight = $(selectors.siteHeader).outerHeight();
1475 var searchOffset = $(selectors.siteHeader).offset().top - searchHeight;
1476
1477 $(selectors.searchDrawer).css({
1478 height: searchHeight + 'px',
1479 top: searchOffset + 'px'
1480 });
1481 });
1482 }
1483
1484 function initDrawer() {
1485 // Add required classes to HTML
1486 $('#PageContainer').addClass('drawer-page-content');
1487 $('.js-drawer-open-top')
1488 .attr('aria-controls', 'SearchDrawer')
1489 .attr('aria-expanded', 'false')
1490 .attr('aria-haspopup', 'dialog');
1491
1492 theme.SearchDrawer = new theme.Drawers('SearchDrawer', 'top', {
1493 onDrawerOpen: searchDrawerFocus,
1494 onDrawerClose: searchDrawerFocusClose
1495 });
1496 }
1497
1498 function searchDrawerFocus() {
1499 searchFocus($(selectors.searchDrawerInput));
1500
1501 if ($(selectors.mobileNavWrapper).hasClass(classes.mobileNavIsOpen)) {
1502 theme.MobileNav.closeMobileNav();
1503 }
1504 }
1505
1506 function searchFocus($el) {
1507 $el.focus();
1508 // set selection range hack for iOS
1509 $el[0].setSelectionRange(0, $el[0].value.length);
1510 }
1511
1512 function searchDrawerFocusClose() {
1513 $(selectors.siteHeaderSearchToggle).focus();
1514 }
1515
1516 /**
1517 * Remove the aria-attributes and hide the error messages
1518 */
1519 function hideErrorMessage() {
1520 this.$searchErrorMessage.addClass(classes.hidden);
1521 this.$searchResultInput
1522 .removeAttr('aria-describedby')
1523 .removeAttr('aria-invalid');
1524 }
1525
1526 /**
1527 * Add the aria-attributes and show the error messages
1528 */
1529 function showErrorMessage() {
1530 this.$searchErrorMessage.removeClass(classes.hidden);
1531 this.$searchResultInput
1532 .attr('aria-describedby', 'error-search-form')
1533 .attr('aria-invalid', true);
1534 }
1535
1536 function validateSearchResultForm(evt) {
1537 var isInputValueEmpty = this.$searchResultInput.val().trim().length === 0;
1538
1539 if (!isInputValueEmpty) {
1540 hideErrorMessage.call(this);
1541 return;
1542 }
1543
1544 if (typeof evt !== 'undefined') {
1545 evt.preventDefault();
1546 }
1547
1548 searchFocus(this.$searchResultInput);
1549 showErrorMessage.call(this);
1550 }
1551
1552 return {
1553 init: init
1554 };
1555})();
1556
1557(function() {
1558 var selectors = {
1559 backButton: '.return-link'
1560 };
1561
1562 var $backButton = $(selectors.backButton);
1563
1564 if (!document.referrer || !$backButton.length || !window.history.length) {
1565 return;
1566 }
1567
1568 $backButton.one('click', function(evt) {
1569 evt.preventDefault();
1570
1571 var referrerDomain = urlDomain(document.referrer);
1572 var shopDomain = urlDomain(window.location.href);
1573
1574 if (shopDomain === referrerDomain) {
1575 history.back();
1576 }
1577
1578 return false;
1579 });
1580
1581 function urlDomain(url) {
1582 var anchor = document.createElement('a');
1583 anchor.ref = url;
1584
1585 return anchor.hostname;
1586 }
1587})();
1588
1589theme.Slideshow = (function() {
1590 this.$slideshow = null;
1591 var classes = {
1592 slideshow: 'slideshow',
1593 slickActiveMobile: 'slick-active-mobile',
1594 controlsHover: 'slideshow__controls--hover',
1595 isPaused: 'is-paused'
1596 };
1597
1598 var selectors = {
1599 section: '.shopify-section',
1600 wrapper: '#SlideshowWrapper-',
1601 slides: '.slideshow__slide',
1602 textWrapperMobile: '.slideshow__text-wrap--mobile',
1603 textContentMobile: '.slideshow__text-content--mobile',
1604 controls: '.slideshow__controls',
1605 pauseButton: '.slideshow__pause',
1606 dots: '.slick-dots',
1607 arrows: '.slideshow__arrows',
1608 arrowsMobile: '.slideshow__arrows--mobile',
1609 arrowLeft: '.slideshow__arrow-left',
1610 arrowRight: '.slideshow__arrow-right'
1611 };
1612
1613 function slideshow(el, sectionId) {
1614 var $slideshow = (this.$slideshow = $(el));
1615 this.adaptHeight = this.$slideshow.data('adapt-height');
1616 this.$wrapper = this.$slideshow.closest(selectors.wrapper + sectionId);
1617 this.$section = this.$wrapper.closest(selectors.section);
1618 this.$controls = this.$wrapper.find(selectors.controls);
1619 this.$arrows = this.$section.find(selectors.arrows);
1620 this.$arrowsMobile = this.$section.find(selectors.arrowsMobile);
1621 this.$pause = this.$controls.find(selectors.pauseButton);
1622 this.$textWrapperMobile = this.$section.find(selectors.textWrapperMobile);
1623 this.autorotate = this.$slideshow.data('autorotate');
1624 var autoplaySpeed = this.$slideshow.data('speed');
1625 var loadSlideA11yString = this.$slideshow.data('slide-nav-a11y');
1626
1627 this.settings = {
1628 accessibility: true,
1629 arrows: false,
1630 dots: true,
1631 fade: true,
1632 draggable: true,
1633 touchThreshold: 20,
1634 autoplay: this.autorotate,
1635 autoplaySpeed: autoplaySpeed,
1636 // eslint-disable-next-line shopify/jquery-dollar-sign-reference
1637 appendDots: this.$arrows,
1638 customPaging: function(slick, index) {
1639 return (
1640 '<a href="' +
1641 selectors.wrapper +
1642 sectionId +
1643 '" aria-label="' +
1644 loadSlideA11yString.replace('[slide_number]', index + 1) +
1645 '" data-slide-number="' +
1646 index +
1647 '"></a>'
1648 );
1649 }
1650 };
1651
1652 this.$slideshow.on('beforeChange', beforeChange.bind(this));
1653 this.$slideshow.on('init', slideshowA11ySetup.bind(this));
1654
1655 // Add class to style mobile dots & show the correct text content for the
1656 // first slide on mobile when the slideshow initialises
1657 this.$slideshow.on(
1658 'init',
1659 function() {
1660 this.$mobileDots
1661 .find('li:first-of-type')
1662 .addClass(classes.slickActiveMobile);
1663 this.showMobileText(0);
1664 }.bind(this)
1665 );
1666
1667 // Stop the autorotate when you scroll past the mobile controls, resume when
1668 // they are scrolled back into view
1669 if (this.autorotate) {
1670 $(document).scroll(
1671 $.debounce(
1672 250,
1673 function() {
1674 if (
1675 this.$arrowsMobile.offset().top +
1676 this.$arrowsMobile.outerHeight() <
1677 window.pageYOffset
1678 ) {
1679 $slideshow.slick('slickPause');
1680 } else if (!this.$pause.hasClass(classes.isPaused)) {
1681 $slideshow.slick('slickPlay');
1682 }
1683 }.bind(this)
1684 )
1685 );
1686 }
1687
1688 if (this.adaptHeight) {
1689 this.setSlideshowHeight();
1690 $(window).resize($.debounce(50, this.setSlideshowHeight.bind(this)));
1691 }
1692
1693 this.$slideshow.slick(this.settings);
1694
1695 // This can't be called when the slick 'init' event fires due to how slick
1696 // adds a11y features.
1697 slideshowPostInitA11ySetup.bind(this)();
1698
1699 this.$arrows.find(selectors.arrowLeft).on('click', function() {
1700 $slideshow.slick('slickPrev');
1701 });
1702 this.$arrows.find(selectors.arrowRight).on('click', function() {
1703 $slideshow.slick('slickNext');
1704 });
1705
1706 this.$pause.on('click', this.togglePause.bind(this));
1707 }
1708
1709 function slideshowA11ySetup(event, obj) {
1710 var $slider = obj.$slider;
1711 var $list = obj.$list;
1712 this.$dots = this.$section.find(selectors.dots);
1713 this.$mobileDots = this.$dots.eq(1);
1714
1715 // Remove default Slick aria-live attr until slider is focused
1716 $list.removeAttr('aria-live');
1717
1718 this.$wrapper.on('keyup', keyboardNavigation.bind(this));
1719 this.$controls.on('keyup', keyboardNavigation.bind(this));
1720 this.$textWrapperMobile.on('keyup', keyboardNavigation.bind(this));
1721
1722 // When an element in the slider is focused
1723 // pause slideshow and set aria-live.
1724 this.$wrapper
1725 .on(
1726 'focusin',
1727 function(evt) {
1728 if (!this.$wrapper.has(evt.target).length) {
1729 return;
1730 }
1731
1732 $list.attr('aria-live', 'polite');
1733 if (this.autorotate) {
1734 $slider.slick('slickPause');
1735 }
1736 }.bind(this)
1737 )
1738 .on(
1739 'focusout',
1740 function(evt) {
1741 if (!this.$wrapper.has(evt.target).length) {
1742 return;
1743 }
1744
1745 $list.removeAttr('aria-live');
1746 if (this.autorotate) {
1747 // Only resume playing if the user hasn't paused using the pause
1748 // button
1749 if (!this.$pause.is('.is-paused')) {
1750 $slider.slick('slickPlay');
1751 }
1752 }
1753 }.bind(this)
1754 );
1755
1756 // Add arrow key support when focused
1757 if (this.$dots) {
1758 this.$dots
1759 .find('a')
1760 .each(function() {
1761 var $dot = $(this);
1762 $dot.on('click keyup', function(evt) {
1763 if (
1764 evt.type === 'keyup' &&
1765 evt.which !== slate.utils.keyboardKeys.ENTER
1766 )
1767 return;
1768
1769 evt.preventDefault();
1770
1771 var slideNumber = $(evt.target).data('slide-number');
1772
1773 $slider.attr('tabindex', -1).slick('slickGoTo', slideNumber);
1774
1775 if (evt.type === 'keyup') {
1776 $slider.focus();
1777 }
1778 });
1779 })
1780 .eq(0)
1781 .attr('aria-current', 'true');
1782 }
1783
1784 this.$controls
1785 .on('focusin', highlightControls.bind(this))
1786 .on('focusout', unhighlightControls.bind(this));
1787 }
1788
1789 function slideshowPostInitA11ySetup() {
1790 var $slides = this.$slideshow.find(selectors.slides);
1791
1792 $slides.removeAttr('role').removeAttr('aria-labelledby');
1793 this.$dots
1794 .removeAttr('role')
1795 .find('li')
1796 .removeAttr('role')
1797 .removeAttr('aria-selected')
1798 .each(function() {
1799 var $dot = $(this);
1800 var ariaControls = $dot.attr('aria-controls');
1801 $dot
1802 .removeAttr('aria-controls')
1803 .find('a')
1804 .attr('aria-controls', ariaControls);
1805 });
1806 }
1807
1808 function beforeChange(event, slick, currentSlide, nextSlide) {
1809 var $dotLinks = this.$dots.find('a');
1810 var $mobileDotLinks = this.$mobileDots.find('li');
1811
1812 $dotLinks
1813 .removeAttr('aria-current')
1814 .eq(nextSlide)
1815 .attr('aria-current', 'true');
1816
1817 $mobileDotLinks
1818 .removeClass(classes.slickActiveMobile)
1819 .eq(nextSlide)
1820 .addClass(classes.slickActiveMobile);
1821 this.showMobileText(nextSlide);
1822 }
1823
1824 function keyboardNavigation() {
1825 if (event.keyCode === slate.utils.keyboardKeys.LEFTARROW) {
1826 this.$slideshow.slick('slickPrev');
1827 }
1828 if (event.keyCode === slate.utils.keyboardKeys.RIGHTARROW) {
1829 this.$slideshow.slick('slickNext');
1830 }
1831 }
1832
1833 function highlightControls() {
1834 this.$controls.addClass(classes.controlsHover);
1835 }
1836
1837 function unhighlightControls() {
1838 this.$controls.removeClass(classes.controlsHover);
1839 }
1840
1841 slideshow.prototype.togglePause = function() {
1842 var slideshowSelector = getSlideshowId(this.$pause);
1843 if (this.$pause.hasClass(classes.isPaused)) {
1844 this.$pause.removeClass(classes.isPaused).attr('aria-pressed', 'false');
1845 if (this.autorotate) {
1846 $(slideshowSelector).slick('slickPlay');
1847 }
1848 } else {
1849 this.$pause.addClass(classes.isPaused).attr('aria-pressed', 'true');
1850 if (this.autorotate) {
1851 $(slideshowSelector).slick('slickPause');
1852 }
1853 }
1854 };
1855
1856 slideshow.prototype.setSlideshowHeight = function() {
1857 var minAspectRatio = this.$slideshow.data('min-aspect-ratio');
1858 this.$slideshow.height($(document).width() / minAspectRatio);
1859 };
1860
1861 slideshow.prototype.showMobileText = function(slideIndex) {
1862 var $allTextContent = this.$textWrapperMobile.find(
1863 selectors.textContentMobile
1864 );
1865 var currentTextContentSelector =
1866 selectors.textContentMobile + '-' + slideIndex;
1867 var $currentTextContent = this.$textWrapperMobile.find(
1868 currentTextContentSelector
1869 );
1870 if (
1871 !$currentTextContent.length &&
1872 this.$slideshow.find(selectors.slides).length === 1
1873 ) {
1874 this.$textWrapperMobile.hide();
1875 } else {
1876 this.$textWrapperMobile.show();
1877 }
1878 $allTextContent.hide();
1879 $currentTextContent.show();
1880 };
1881
1882 function getSlideshowId($el) {
1883 return '#Slideshow-' + $el.data('id');
1884 }
1885
1886 return slideshow;
1887})();
1888
1889// Youtube API callback
1890// eslint-disable-next-line no-unused-vars
1891function onYouTubeIframeAPIReady() {
1892 theme.Video.loadVideos();
1893}
1894
1895theme.Video = (function() {
1896 var autoplayCheckComplete = false;
1897 var playOnClickChecked = false;
1898 var playOnClick = false;
1899 var youtubeLoaded = false;
1900 var videos = {};
1901 var videoPlayers = [];
1902 var videoOptions = {
1903 ratio: 16 / 9,
1904 scrollAnimationDuration: 400,
1905 playerVars: {
1906 // eslint-disable-next-line camelcase
1907 iv_load_policy: 3,
1908 modestbranding: 1,
1909 autoplay: 0,
1910 controls: 0,
1911 wmode: 'opaque',
1912 branding: 0,
1913 autohide: 0,
1914 rel: 0
1915 },
1916 events: {
1917 onReady: onPlayerReady,
1918 onStateChange: onPlayerChange
1919 }
1920 };
1921 var classes = {
1922 playing: 'video-is-playing',
1923 paused: 'video-is-paused',
1924 loading: 'video-is-loading',
1925 loaded: 'video-is-loaded',
1926 backgroundVideoWrapper: 'video-background-wrapper',
1927 videoWithImage: 'video--image_with_play',
1928 backgroundVideo: 'video--background',
1929 userPaused: 'is-paused',
1930 supportsAutoplay: 'autoplay',
1931 supportsNoAutoplay: 'no-autoplay',
1932 wrapperMinHeight: 'video-section-wrapper--min-height'
1933 };
1934
1935 var selectors = {
1936 section: '.video-section',
1937 videoWrapper: '.video-section-wrapper',
1938 playVideoBtn: '.video-control__play',
1939 closeVideoBtn: '.video-control__close-wrapper',
1940 pauseVideoBtn: '.video__pause',
1941 pauseVideoStop: '.video__pause-stop',
1942 pauseVideoResume: '.video__pause-resume',
1943 fallbackText: '.icon__fallback-text'
1944 };
1945
1946 /**
1947 * Public functions
1948 */
1949 function init($video) {
1950 if (!$video.length) {
1951 return;
1952 }
1953
1954 videos[$video.attr('id')] = {
1955 id: $video.attr('id'),
1956 videoId: $video.data('id'),
1957 type: $video.data('type'),
1958 status:
1959 $video.data('type') === 'image_with_play' ? 'closed' : 'background', // closed, open, background
1960 $video: $video,
1961 $videoWrapper: $video.closest(selectors.videoWrapper),
1962 $section: $video.closest(selectors.section),
1963 controls: $video.data('type') === 'background' ? 0 : 1
1964 };
1965
1966 if (!youtubeLoaded) {
1967 // This code loads the IFrame Player API code asynchronously.
1968 var tag = document.createElement('script');
1969 tag.src = 'https://www.youtube.com/iframe_api';
1970 var firstScriptTag = document.getElementsByTagName('script')[0];
1971 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
1972 }
1973
1974 playOnClickCheck();
1975 }
1976
1977 function customPlayVideo(playerId) {
1978 // Make sure we have carried out the playOnClick check first
1979 if (!playOnClickChecked && !playOnClick) {
1980 return;
1981 }
1982
1983 if (playerId && typeof videoPlayers[playerId].playVideo === 'function') {
1984 privatePlayVideo(playerId);
1985 }
1986 }
1987
1988 function pauseVideo(playerId) {
1989 if (
1990 videoPlayers[playerId] &&
1991 typeof videoPlayers[playerId].pauseVideo === 'function'
1992 ) {
1993 videoPlayers[playerId].pauseVideo();
1994 }
1995 }
1996
1997 function loadVideos() {
1998 for (var key in videos) {
1999 if (videos.hasOwnProperty(key)) {
2000 createPlayer(key);
2001 }
2002 }
2003
2004 initEvents();
2005 youtubeLoaded = true;
2006 }
2007
2008 function editorLoadVideo(key) {
2009 if (!youtubeLoaded) {
2010 return;
2011 }
2012 createPlayer(key);
2013
2014 initEvents();
2015 }
2016
2017 /**
2018 * Private functions
2019 */
2020
2021 function privatePlayVideo(id, clicked) {
2022 var videoData = videos[id];
2023 var player = videoPlayers[id];
2024 var $videoWrapper = videoData.$videoWrapper;
2025
2026 if (playOnClick) {
2027 // playOnClick means we are probably on mobile (no autoplay).
2028 // setAsPlaying will show the iframe, requiring another click
2029 // to play the video.
2030 setAsPlaying(videoData);
2031 } else if (clicked || autoplayCheckComplete) {
2032 // Play if autoplay is available or clicked to play
2033 $videoWrapper.removeClass(classes.loading);
2034 setAsPlaying(videoData);
2035 player.playVideo();
2036 return;
2037 } else {
2038 player.playVideo();
2039 }
2040 }
2041
2042 function setAutoplaySupport(supported) {
2043 var supportClass = supported
2044 ? classes.supportsAutoplay
2045 : classes.supportsNoAutoplay;
2046 $(document.documentElement)
2047 .removeClass(classes.supportsAutoplay)
2048 .removeClass(classes.supportsNoAutoplay)
2049 .addClass(supportClass);
2050
2051 if (!supported) {
2052 playOnClick = true;
2053 }
2054
2055 autoplayCheckComplete = true;
2056 }
2057
2058 function playOnClickCheck() {
2059 // Bail early for a few instances:
2060 // - small screen
2061 // - device sniff mobile browser
2062
2063 if (playOnClickChecked) {
2064 return;
2065 }
2066
2067 if (isMobile()) {
2068 playOnClick = true;
2069 }
2070
2071 if (playOnClick) {
2072 // No need to also do the autoplay check
2073 setAutoplaySupport(false);
2074 }
2075
2076 playOnClickChecked = true;
2077 }
2078
2079 // The API will call this function when each video player is ready
2080 function onPlayerReady(evt) {
2081 evt.target.setPlaybackQuality('hd1080');
2082 var videoData = getVideoOptions(evt);
2083 var videoTitle = evt.target.getVideoData().title;
2084 playOnClickCheck();
2085
2086 // Prevent tabbing through YouTube player controls until visible
2087 $('#' + videoData.id).attr('tabindex', '-1');
2088
2089 sizeBackgroundVideos();
2090 setButtonLabels(videoData.$videoWrapper, videoTitle);
2091
2092 // Customize based on options from the video ID
2093 if (videoData.type === 'background') {
2094 evt.target.mute();
2095 privatePlayVideo(videoData.id);
2096 }
2097
2098 videoData.$videoWrapper.addClass(classes.loaded);
2099 }
2100
2101 function onPlayerChange(evt) {
2102 var videoData = getVideoOptions(evt);
2103 if (
2104 videoData.status === 'background' &&
2105 !isMobile() &&
2106 !autoplayCheckComplete &&
2107 (evt.data === YT.PlayerState.PLAYING ||
2108 evt.data === YT.PlayerState.BUFFERING)
2109 ) {
2110 setAutoplaySupport(true);
2111 autoplayCheckComplete = true;
2112 videoData.$videoWrapper.removeClass(classes.loading);
2113 }
2114 switch (evt.data) {
2115 case YT.PlayerState.ENDED:
2116 setAsFinished(videoData);
2117 break;
2118 case YT.PlayerState.PAUSED:
2119 // Seeking on a YouTube video also fires a PAUSED state change,
2120 // checking the state after a delay prevents us pausing the video when
2121 // the user is seeking instead of pausing
2122 setTimeout(function() {
2123 if (evt.target.getPlayerState() === YT.PlayerState.PAUSED) {
2124 setAsPaused(videoData);
2125 }
2126 }, 200);
2127 break;
2128 }
2129 }
2130
2131 function setAsFinished(videoData) {
2132 switch (videoData.type) {
2133 case 'background':
2134 videoPlayers[videoData.id].seekTo(0);
2135 break;
2136 case 'image_with_play':
2137 closeVideo(videoData.id);
2138 toggleExpandVideo(videoData.id, false);
2139 break;
2140 }
2141 }
2142
2143 function setAsPlaying(videoData) {
2144 var $videoWrapper = videoData.$videoWrapper;
2145 var $pauseButton = $videoWrapper.find(selectors.pauseVideoBtn);
2146
2147 $videoWrapper.removeClass(classes.loading);
2148
2149 if ($pauseButton.hasClass(classes.userPaused)) {
2150 $pauseButton.removeClass(classes.userPaused);
2151 }
2152
2153 // Do not change element visibility if it is a background video
2154 if (videoData.status === 'background') {
2155 return;
2156 }
2157
2158 $('#' + videoData.id).attr('tabindex', '0');
2159
2160 if (videoData.type === 'image_with_play') {
2161 $videoWrapper.removeClass(classes.paused).addClass(classes.playing);
2162 }
2163
2164 // Update focus to the close button so we stay within the video wrapper,
2165 // allowing time for the scroll animation
2166 setTimeout(function() {
2167 $videoWrapper.find(selectors.closeVideoBtn).focus();
2168 }, videoOptions.scrollAnimationDuration);
2169 }
2170
2171 function setAsPaused(videoData) {
2172 var $videoWrapper = videoData.$videoWrapper;
2173
2174 // YT's events fire after our click event. This status flag ensures
2175 // we don't interact with a closed or background video.
2176 if (videoData.type === 'image_with_play') {
2177 if (videoData.status === 'closed') {
2178 $videoWrapper.removeClass(classes.paused);
2179 } else {
2180 $videoWrapper.addClass(classes.paused);
2181 }
2182 }
2183
2184 $videoWrapper.removeClass(classes.playing);
2185 }
2186
2187 function closeVideo(playerId) {
2188 var videoData = videos[playerId];
2189 var $videoWrapper = videoData.$videoWrapper;
2190 var classesToRemove = [classes.paused, classes.playing].join(' ');
2191
2192 if (isMobile()) {
2193 $videoWrapper.removeAttr('style');
2194 }
2195
2196 $('#' + videoData.id).attr('tabindex', '-1');
2197
2198 videoData.status = 'closed';
2199
2200 switch (videoData.type) {
2201 case 'image_with_play':
2202 videoPlayers[playerId].stopVideo();
2203 setAsPaused(videoData); // in case the video is already paused
2204 break;
2205 case 'background':
2206 videoPlayers[playerId].mute();
2207 setBackgroundVideo(playerId);
2208 break;
2209 }
2210
2211 $videoWrapper.removeClass(classesToRemove);
2212 }
2213
2214 function getVideoOptions(evt) {
2215 return videos[evt.target.a.id];
2216 }
2217
2218 function toggleExpandVideo(playerId, expand) {
2219 var video = videos[playerId];
2220 var elementTop = video.$videoWrapper.offset().top;
2221 var $playButton = video.$videoWrapper.find(selectors.playVideoBtn);
2222 var offset = 0;
2223 var newHeight = 0;
2224
2225 if (isMobile()) {
2226 video.$videoWrapper.parent().toggleClass('page-width', !expand);
2227 }
2228
2229 if (expand) {
2230 if (isMobile()) {
2231 newHeight = $(window).width() / videoOptions.ratio;
2232 } else {
2233 newHeight = video.$videoWrapper.width() / videoOptions.ratio;
2234 }
2235 offset = ($(window).height() - newHeight) / 2;
2236
2237 video.$videoWrapper
2238 .removeClass(classes.wrapperMinHeight)
2239 .animate({ height: newHeight }, 600);
2240
2241 // Animate doesn't work in mobile editor, so we don't use it
2242 if (!(isMobile() && Shopify.designMode)) {
2243 $('html, body').animate(
2244 {
2245 scrollTop: elementTop - offset
2246 },
2247 videoOptions.scrollAnimationDuration
2248 );
2249 }
2250 } else {
2251 if (isMobile()) {
2252 newHeight = video.$videoWrapper.data('mobile-height');
2253 } else {
2254 newHeight = video.$videoWrapper.data('desktop-height');
2255 }
2256
2257 video.$videoWrapper
2258 .height(video.$videoWrapper.width() / videoOptions.ratio)
2259 .animate({ height: newHeight }, 600);
2260 setTimeout(function() {
2261 video.$videoWrapper.addClass(classes.wrapperMinHeight);
2262 }, 600);
2263 $playButton.focus();
2264 }
2265 }
2266
2267 function togglePause(playerId) {
2268 var $pauseButton = videos[playerId].$videoWrapper.find(
2269 selectors.pauseVideoBtn
2270 );
2271 var paused = $pauseButton.hasClass(classes.userPaused);
2272 if (paused) {
2273 $pauseButton.removeClass(classes.userPaused);
2274 customPlayVideo(playerId);
2275 } else {
2276 $pauseButton.addClass(classes.userPaused);
2277 pauseVideo(playerId);
2278 }
2279 $pauseButton.attr('aria-pressed', !paused);
2280 }
2281
2282 function startVideoOnClick(playerId) {
2283 var video = videos[playerId];
2284
2285 // add loading class to wrapper
2286 video.$videoWrapper.addClass(classes.loading);
2287
2288 // Explicity set the video wrapper height (needed for height transition)
2289 video.$videoWrapper.attr(
2290 'style',
2291 'height: ' + video.$videoWrapper.height() + 'px'
2292 );
2293
2294 video.status = 'open';
2295
2296 switch (video.type) {
2297 case 'image_with_play':
2298 privatePlayVideo(playerId, true);
2299 break;
2300 case 'background':
2301 unsetBackgroundVideo(playerId, video);
2302 videoPlayers[playerId].unMute();
2303 privatePlayVideo(playerId, true);
2304 break;
2305 }
2306
2307 toggleExpandVideo(playerId, true);
2308
2309 // esc to close video player
2310 $(document).on('keydown.videoPlayer', function(evt) {
2311 var playerId = $(document.activeElement).data('controls');
2312 if (evt.keyCode !== slate.utils.keyboardKeys.ESCAPE || !playerId) {
2313 return;
2314 }
2315
2316 closeVideo(playerId);
2317 toggleExpandVideo(playerId, false);
2318 });
2319 }
2320
2321 function sizeBackgroundVideos() {
2322 $('.' + classes.backgroundVideo).each(function(index, el) {
2323 sizeBackgroundVideo($(el));
2324 });
2325 }
2326
2327 function sizeBackgroundVideo($videoPlayer) {
2328 if (!youtubeLoaded) {
2329 return;
2330 }
2331
2332 if (isMobile()) {
2333 $videoPlayer.removeAttr('style');
2334 } else {
2335 var $videoWrapper = $videoPlayer.closest(selectors.videoWrapper);
2336 var videoWidth = $videoWrapper.width();
2337 var playerWidth = $videoPlayer.width();
2338 var desktopHeight = $videoWrapper.data('desktop-height');
2339
2340 // when screen aspect ratio differs from video, video must center and underlay one dimension
2341 if (videoWidth / videoOptions.ratio < desktopHeight) {
2342 playerWidth = Math.ceil(desktopHeight * videoOptions.ratio); // get new player width
2343 $videoPlayer
2344 .width(playerWidth)
2345 .height(desktopHeight)
2346 .css({
2347 left: (videoWidth - playerWidth) / 2,
2348 top: 0
2349 }); // player width is greater, offset left; reset top
2350 } else {
2351 // new video width < window width (gap to right)
2352 desktopHeight = Math.ceil(videoWidth / videoOptions.ratio); // get new player height
2353 $videoPlayer
2354 .width(videoWidth)
2355 .height(desktopHeight)
2356 .css({
2357 left: 0,
2358 top: (desktopHeight - desktopHeight) / 2
2359 }); // player height is greater, offset top; reset left
2360 }
2361
2362 $videoPlayer.prepareTransition();
2363 $videoWrapper.addClass(classes.loaded);
2364 }
2365 }
2366
2367 function unsetBackgroundVideo(playerId) {
2368 // Switch the background video to a chrome-only player once played
2369 $('#' + playerId)
2370 .removeClass(classes.backgroundVideo)
2371 .addClass(classes.videoWithImage);
2372
2373 setTimeout(function() {
2374 $('#' + playerId).removeAttr('style');
2375 }, 600);
2376
2377 videos[playerId].$videoWrapper
2378 .removeClass(classes.backgroundVideoWrapper)
2379 .addClass(classes.playing);
2380
2381 videos[playerId].status = 'open';
2382 }
2383
2384 function setBackgroundVideo(playerId) {
2385 $('#' + playerId)
2386 .removeClass(classes.videoWithImage)
2387 .addClass(classes.backgroundVideo);
2388
2389 videos[playerId].$videoWrapper.addClass(classes.backgroundVideoWrapper);
2390
2391 videos[playerId].status = 'background';
2392 sizeBackgroundVideo($('#' + playerId));
2393 }
2394
2395 function isMobile() {
2396 return $(window).width() < 750 || window.mobileCheck();
2397 }
2398
2399 function initEvents() {
2400 $(document).on('click.videoPlayer', selectors.playVideoBtn, function(evt) {
2401 var playerId = $(evt.currentTarget).data('controls');
2402
2403 startVideoOnClick(playerId);
2404 });
2405
2406 $(document).on('click.videoPlayer', selectors.closeVideoBtn, function(evt) {
2407 var playerId = $(evt.currentTarget).data('controls');
2408
2409 $(evt.currentTarget).blur();
2410 closeVideo(playerId);
2411 toggleExpandVideo(playerId, false);
2412 });
2413
2414 $(document).on('click.videoPlayer', selectors.pauseVideoBtn, function(evt) {
2415 var playerId = $(evt.currentTarget).data('controls');
2416 togglePause(playerId);
2417 });
2418
2419 // Listen to resize to keep a background-size:cover-like layout
2420 $(window).on(
2421 'resize.videoPlayer',
2422 $.debounce(200, function() {
2423 if (!youtubeLoaded) return;
2424 var key;
2425 var fullscreen = window.innerHeight === screen.height;
2426
2427 sizeBackgroundVideos();
2428
2429 if (isMobile()) {
2430 for (key in videos) {
2431 if (videos.hasOwnProperty(key)) {
2432 if (videos[key].$videoWrapper.hasClass(classes.playing)) {
2433 if (!fullscreen) {
2434 pauseVideo(key);
2435 setAsPaused(videos[key]);
2436 }
2437 }
2438 videos[key].$videoWrapper.height(
2439 $(document).width() / videoOptions.ratio
2440 );
2441 }
2442 }
2443 setAutoplaySupport(false);
2444 } else {
2445 setAutoplaySupport(true);
2446 for (key in videos) {
2447 if (
2448 videos[key].$videoWrapper.find('.' + classes.videoWithImage)
2449 .length
2450 ) {
2451 continue;
2452 }
2453 videoPlayers[key].playVideo();
2454 setAsPlaying(videos[key]);
2455 }
2456 }
2457 })
2458 );
2459
2460 $(window).on(
2461 'scroll.videoPlayer',
2462 $.debounce(50, function() {
2463 if (!youtubeLoaded) return;
2464
2465 for (var key in videos) {
2466 if (videos.hasOwnProperty(key)) {
2467 var $videoWrapper = videos[key].$videoWrapper;
2468
2469 // Close the video if more than 75% of it is scrolled out of view
2470 if (
2471 $videoWrapper.hasClass(classes.playing) &&
2472 ($videoWrapper.offset().top + $videoWrapper.height() * 0.75 <
2473 $(window).scrollTop() ||
2474 $videoWrapper.offset().top + $videoWrapper.height() * 0.25 >
2475 $(window).scrollTop() + $(window).height())
2476 ) {
2477 closeVideo(key);
2478 toggleExpandVideo(key, false);
2479 }
2480 }
2481 }
2482 })
2483 );
2484 }
2485
2486 function createPlayer(key) {
2487 var args = $.extend({}, videoOptions, videos[key]);
2488 args.playerVars.controls = args.controls;
2489 videoPlayers[key] = new YT.Player(key, args);
2490 }
2491
2492 function removeEvents() {
2493 $(document).off('.videoPlayer');
2494 $(window).off('.videoPlayer');
2495 }
2496
2497 function setButtonLabels($videoWrapper, title) {
2498 var $playButtons = $videoWrapper.find(selectors.playVideoBtn);
2499 var $closeButton = $videoWrapper.find(selectors.closeVideoBtn);
2500 var $pauseButton = $videoWrapper.find(selectors.pauseVideoBtn);
2501 var $closeButtonText = $closeButton.find(selectors.fallbackText);
2502 var $pauseButtonStopText = $pauseButton
2503 .find(selectors.pauseVideoStop)
2504 .find(selectors.fallbackText);
2505 var $pauseButtonResumeText = $pauseButton
2506 .find(selectors.pauseVideoResume)
2507 .find(selectors.fallbackText);
2508
2509 // Insert the video title retrieved from YouTube into the instructional text
2510 // for each button
2511 $playButtons.each(function() {
2512 var $playButton = $(this);
2513 var $playButtonText = $playButton.find(selectors.fallbackText);
2514
2515 $playButtonText.text(
2516 $playButtonText.text().replace('[video_title]', title)
2517 );
2518 });
2519 $closeButtonText.text(
2520 $closeButtonText.text().replace('[video_title]', title)
2521 );
2522 $pauseButtonStopText.text(
2523 $pauseButtonStopText.text().replace('[video_title]', title)
2524 );
2525 $pauseButtonResumeText.text(
2526 $pauseButtonResumeText.text().replace('[video_title]', title)
2527 );
2528 }
2529
2530 return {
2531 init: init,
2532 editorLoadVideo: editorLoadVideo,
2533 loadVideos: loadVideos,
2534 playVideo: customPlayVideo,
2535 pauseVideo: pauseVideo,
2536 removeEvents: removeEvents
2537 };
2538})();
2539
2540window.theme = window.theme || {};
2541
2542theme.FormStatus = (function() {
2543 var selectors = {
2544 statusMessage: '[data-form-status]'
2545 };
2546
2547 function init() {
2548 this.$statusMessage = $(selectors.statusMessage);
2549
2550 if (!this.$statusMessage) return;
2551
2552 this.$statusMessage.attr('tabindex', -1).focus();
2553
2554 this.$statusMessage.on('blur', handleBlur.bind(this));
2555 }
2556
2557 function handleBlur() {
2558 this.$statusMessage.removeAttr('tabindex');
2559 }
2560
2561 return {
2562 init: init
2563 };
2564})();
2565
2566theme.Hero = (function() {
2567 var classes = {
2568 indexSectionFlush: 'index-section--flush'
2569 };
2570
2571 var selectors = {
2572 heroFixedWidthContent: '.hero-fixed-width__content',
2573 heroFixedWidthImage: '.hero-fixed-width__image'
2574 };
2575
2576 function hero(el, sectionId) {
2577 this.$hero = $(el);
2578 this.layout = this.$hero.data('layout');
2579 var $parentSection = $('#shopify-section-' + sectionId);
2580 var $heroContent = $parentSection.find(selectors.heroFixedWidthContent);
2581 var $heroImage = $parentSection.find(selectors.heroFixedWidthImage);
2582
2583 if (this.layout !== 'fixed_width') {
2584 return;
2585 }
2586
2587 $parentSection.removeClass(classes.indexSectionFlush);
2588 heroFixedHeight();
2589 $(window).resize(
2590 $.debounce(50, function() {
2591 heroFixedHeight();
2592 })
2593 );
2594
2595 function heroFixedHeight() {
2596 var contentHeight = $heroContent.height() + 50;
2597 var imageHeight = $heroImage.height();
2598
2599 if (contentHeight > imageHeight) {
2600 $heroImage.css('min-height', contentHeight);
2601 }
2602 }
2603 }
2604
2605 return hero;
2606})();
2607
2608
2609/* ================ TEMPLATES ================ */
2610(function() {
2611 var $filterBy = $('#BlogTagFilter');
2612
2613 if (!$filterBy.length) {
2614 return;
2615 }
2616
2617 $filterBy.on('change', function() {
2618 location.href = $(this).val();
2619 });
2620})();
2621
2622window.theme = theme || {};
2623
2624theme.customerTemplates = (function() {
2625 var selectors = {
2626 RecoverHeading: '#RecoverHeading',
2627 RecoverEmail: '#RecoverEmail',
2628 LoginHeading: '#LoginHeading'
2629 };
2630
2631 function initEventListeners() {
2632 this.$RecoverHeading = $(selectors.RecoverHeading);
2633 this.$RecoverEmail = $(selectors.RecoverEmail);
2634 this.$LoginHeading = $(selectors.LoginHeading);
2635
2636 // Show reset password form
2637 $('#RecoverPassword').on(
2638 'click',
2639 function(evt) {
2640 evt.preventDefault();
2641 showRecoverPasswordForm();
2642 this.$RecoverHeading.attr('tabindex', '-1').focus();
2643 }.bind(this)
2644 );
2645
2646 // Hide reset password form
2647 $('#HideRecoverPasswordLink').on(
2648 'click',
2649 function(evt) {
2650 evt.preventDefault();
2651 hideRecoverPasswordForm();
2652 this.$LoginHeading.attr('tabindex', '-1').focus();
2653 }.bind(this)
2654 );
2655
2656 this.$RecoverHeading.on('blur', function() {
2657 $(this).removeAttr('tabindex');
2658 });
2659
2660 this.$LoginHeading.on('blur', function() {
2661 $(this).removeAttr('tabindex');
2662 });
2663 }
2664
2665 /**
2666 *
2667 * Show/Hide recover password form
2668 *
2669 */
2670
2671 function showRecoverPasswordForm() {
2672 $('#RecoverPasswordForm').removeClass('hide');
2673 $('#CustomerLoginForm').addClass('hide');
2674
2675 if (this.$RecoverEmail.attr('aria-invalid') === 'true') {
2676 this.$RecoverEmail.focus();
2677 }
2678 }
2679
2680 function hideRecoverPasswordForm() {
2681 $('#RecoverPasswordForm').addClass('hide');
2682 $('#CustomerLoginForm').removeClass('hide');
2683 }
2684
2685 /**
2686 *
2687 * Show reset password success message
2688 *
2689 */
2690 function resetPasswordSuccess() {
2691 var $formState = $('.reset-password-success');
2692
2693 // check if reset password form was successfully submited.
2694 if (!$formState.length) {
2695 return;
2696 }
2697
2698 // show success message
2699 $('#ResetSuccess')
2700 .removeClass('hide')
2701 .focus();
2702 }
2703
2704 /**
2705 *
2706 * Show/hide customer address forms
2707 *
2708 */
2709 function customerAddressForm() {
2710 var $newAddressForm = $('#AddressNewForm');
2711 var $newAddressFormButton = $('#AddressNewButton');
2712
2713 if (!$newAddressForm.length) {
2714 return;
2715 }
2716
2717 // Initialize observers on address selectors, defined in shopify_common.js
2718 if (Shopify) {
2719 // eslint-disable-next-line no-new
2720 new Shopify.CountryProvinceSelector(
2721 'AddressCountryNew',
2722 'AddressProvinceNew',
2723 {
2724 hideElement: 'AddressProvinceContainerNew'
2725 }
2726 );
2727 }
2728
2729 // Initialize each edit form's country/province selector
2730 $('.address-country-option').each(function() {
2731 var formId = $(this).data('form-id');
2732 var countrySelector = 'AddressCountry_' + formId;
2733 var provinceSelector = 'AddressProvince_' + formId;
2734 var containerSelector = 'AddressProvinceContainer_' + formId;
2735
2736 // eslint-disable-next-line no-new
2737 new Shopify.CountryProvinceSelector(countrySelector, provinceSelector, {
2738 hideElement: containerSelector
2739 });
2740 });
2741
2742 // Toggle new/edit address forms
2743 $('.address-new-toggle').on('click', function() {
2744 var isExpanded = $newAddressFormButton.attr('aria-expanded') === 'true';
2745
2746 $newAddressForm.toggleClass('hide');
2747 $newAddressFormButton.attr('aria-expanded', !isExpanded).focus();
2748 });
2749
2750 $('.address-edit-toggle').on('click', function() {
2751 var formId = $(this).data('form-id');
2752 var $editButton = $('#EditFormButton_' + formId);
2753 var $editAddress = $('#EditAddress_' + formId);
2754 var isExpanded = $editButton.attr('aria-expanded') === 'true';
2755
2756 $editAddress.toggleClass('hide');
2757 $editButton.attr('aria-expanded', !isExpanded).focus();
2758 });
2759
2760 $('.address-delete').on('click', function() {
2761 var $el = $(this);
2762 var target = $el.data('target');
2763 var confirmMessage = $el.data('confirm-message');
2764
2765 // eslint-disable-next-line no-alert
2766 if (
2767 confirm(
2768 confirmMessage || 'Are you sure you wish to delete this address?'
2769 )
2770 ) {
2771 Shopify.postLink(target, {
2772 parameters: { _method: 'delete' }
2773 });
2774 }
2775 });
2776 }
2777
2778 /**
2779 *
2780 * Check URL for reset password hash
2781 *
2782 */
2783 function checkUrlHash() {
2784 var hash = window.location.hash;
2785
2786 // Allow deep linking to recover password form
2787 if (hash === '#recover') {
2788 showRecoverPasswordForm.bind(this)();
2789 }
2790 }
2791
2792 return {
2793 init: function() {
2794 initEventListeners();
2795 checkUrlHash();
2796 resetPasswordSuccess();
2797 customerAddressForm();
2798 }
2799 };
2800})();
2801
2802
2803/*================ SECTIONS ================*/
2804window.theme = window.theme || {};
2805
2806theme.Cart = (function() {
2807 var selectors = {
2808 cartCount: '[data-cart-count]',
2809 cartCountBubble: '[data-cart-count-bubble]',
2810 cartDiscount: '[data-cart-discount]',
2811 cartDiscountTitle: '[data-cart-discount-title]',
2812 cartDiscountAmount: '[data-cart-discount-amount]',
2813 cartDiscountWrapper: '[data-cart-discount-wrapper]',
2814 cartErrorMessage: '[data-cart-error-message]',
2815 cartErrorMessageWrapper: '[data-cart-error-message-wrapper]',
2816 cartItem: '[data-cart-item]',
2817 cartItemDetails: '[data-cart-item-details]',
2818 cartItemDiscount: '[data-cart-item-discount]',
2819 cartItemDiscountedPriceGroup: '[data-cart-item-discounted-price-group]',
2820 cartItemDiscountTitle: '[data-cart-item-discount-title]',
2821 cartItemDiscountAmount: '[data-cart-item-discount-amount]',
2822 cartItemDiscountList: '[data-cart-item-discount-list]',
2823 cartItemFinalPrice: '[data-cart-item-final-price]',
2824 cartItemImage: '[data-cart-item-image]',
2825 cartItemLinePrice: '[data-cart-item-line-price]',
2826 cartItemOriginalPrice: '[data-cart-item-original-price]',
2827 cartItemPrice: '[data-cart-item-price]',
2828 cartItemPriceList: '[data-cart-item-price-list]',
2829 cartItemProperty: '[data-cart-item-property]',
2830 cartItemPropertyName: '[data-cart-item-property-name]',
2831 cartItemPropertyValue: '[data-cart-item-property-value]',
2832 cartItemRegularPriceGroup: '[data-cart-item-regular-price-group]',
2833 cartItemRegularPrice: '[data-cart-item-regular-price]',
2834 cartItemTitle: '[data-cart-item-title]',
2835 cartItemOption: '[data-cart-item-option]',
2836 cartLineItems: '[data-cart-line-items]',
2837 cartNote: '[data-cart-notes]',
2838 cartQuantityErrorMessage: '[data-cart-quantity-error-message]',
2839 cartQuantityErrorMessageWrapper:
2840 '[data-cart-quantity-error-message-wrapper]',
2841 cartRemove: '[data-cart-remove]',
2842 cartStatus: '[data-cart-status]',
2843 cartSubtotal: '[data-cart-subtotal]',
2844 cartTableCell: '[data-cart-table-cell]',
2845 cartWrapper: '[data-cart-wrapper]',
2846 emptyPageContent: '[data-empty-page-content]',
2847 quantityInput: '[data-quantity-input]',
2848 quantityInputMobile: '[data-quantity-input-mobile]',
2849 quantityInputDesktop: '[data-quantity-input-desktop]',
2850 quantityLabelMobile: '[data-quantity-label-mobile]',
2851 quantityLabelDesktop: '[data-quantity-label-desktop]',
2852 inputQty: '[data-quantity-input]',
2853 thumbnails: '.cart__image',
2854 unitPrice: '[data-unit-price]',
2855 unitPriceBaseUnit: '[data-unit-price-base-unit]',
2856 unitPriceGroup: '[data-unit-price-group]'
2857 };
2858
2859 var classes = {
2860 cartNoCookies: 'cart--no-cookies',
2861 cartRemovedProduct: 'cart__removed-product',
2862 hide: 'hide',
2863 inputError: 'input--error'
2864 };
2865
2866 var attributes = {
2867 cartItemIndex: 'data-cart-item-index',
2868 cartItemKey: 'data-cart-item-key',
2869 cartItemQuantity: 'data-cart-item-quantity',
2870 cartItemTitle: 'data-cart-item-title',
2871 cartItemUrl: 'data-cart-item-url',
2872 quantityItem: 'data-quantity-item'
2873 };
2874
2875 var mediumUpQuery = '(min-width: ' + theme.breakpoints.medium + 'px)';
2876
2877 function Cart(container) {
2878 this.$container = $(container);
2879 this.$thumbnails = $(selectors.thumbnails, this.$container);
2880 this.ajaxEnabled = this.$container.data('ajax-enabled');
2881
2882 if (!this.cookiesEnabled()) {
2883 this.$container.addClass(classes.cartNoCookies);
2884 }
2885
2886 this.$thumbnails.css('cursor', 'pointer');
2887 this.$container.on(
2888 'click',
2889 selectors.thumbnails,
2890 this._handleThumbnailClick
2891 );
2892
2893 this.$container.on(
2894 'change',
2895 selectors.inputQty,
2896 $.debounce(500, this._handleInputQty.bind(this))
2897 );
2898
2899 this.mql = window.matchMedia(mediumUpQuery);
2900 this.mql.addListener(this.setQuantityFormControllers.bind(this));
2901 this.setQuantityFormControllers();
2902
2903 if (this.ajaxEnabled) {
2904 /**
2905 * Because the entire cart is recreated when a cart item is updated,
2906 * we cannot cache the elements in the cart. Instead, we add the event
2907 * listeners on the cart's container to allow us to retain the event
2908 * listeners after rebuilding the cart when an item is updated.
2909 */
2910
2911 this.$container.on(
2912 'change',
2913 selectors.cartNote,
2914 this._onNoteChange.bind(this)
2915 );
2916
2917 this.$container.on(
2918 'click',
2919 selectors.cartRemove,
2920 this._onRemoveItem.bind(this)
2921 );
2922
2923 this._setupCartTemplates();
2924 }
2925 }
2926
2927 Cart.prototype = _.assignIn({}, Cart.prototype, {
2928 _setupCartTemplates: function() {
2929 this.$itemTemplate = $(selectors.cartItem, this.$container)
2930 .first()
2931 .clone();
2932 this.$itemDiscountTemplate = $(
2933 selectors.cartItemDiscount,
2934 this.$itemTemplate
2935 ).clone();
2936 this.$itemOptionTemplate = $(
2937 selectors.cartItemOption,
2938 this.$itemTemplate
2939 ).clone();
2940 this.$itemPropertyTemplate = $(
2941 selectors.cartItemProperty,
2942 this.$itemTemplate
2943 ).clone();
2944 this.$itemPriceListTemplate = $(
2945 selectors.cartItemPriceList,
2946 this.$itemTemplate
2947 ).clone();
2948 this.$itemLinePriceTemplate = $(
2949 selectors.cartItemLinePrice,
2950 this.$itemTemplate
2951 ).clone();
2952 this.$cartDiscountTemplate = $(
2953 selectors.cartDiscount,
2954 this.$container
2955 ).clone();
2956 },
2957
2958 _handleInputQty: function(evt) {
2959 var $input = $(evt.target);
2960 var itemIndex = $input.data('quantity-item');
2961 var $itemElement = $input.closest(selectors.cartItem);
2962 var $itemQtyInputs = $('[data-quantity-item=' + itemIndex + ']');
2963 var value = parseInt($input.val());
2964 var isValidValue = !(value < 0 || isNaN(value));
2965 $itemQtyInputs.val(value);
2966
2967 this._hideCartError();
2968 this._hideQuantityErrorMessage();
2969
2970 if (!isValidValue) {
2971 this._showQuantityErrorMessages($itemElement);
2972 return;
2973 }
2974
2975 if (isValidValue && this.ajaxEnabled) {
2976 this._updateItemQuantity(
2977 itemIndex,
2978 $itemElement,
2979 $itemQtyInputs,
2980 value
2981 );
2982 }
2983 },
2984
2985 _updateItemQuantity: function(
2986 itemIndex,
2987 $itemElement,
2988 $itemQtyInputs,
2989 value
2990 ) {
2991 var key = $itemElement.attr(attributes.cartItemKey);
2992 var index = $itemElement.attr(attributes.cartItemIndex);
2993
2994 var params = {
2995 url: '/cart/change.js',
2996 data: { quantity: value, line: index },
2997 dataType: 'json'
2998 };
2999
3000 $.post(params)
3001 .done(
3002 function(state) {
3003 if (state.item_count === 0) {
3004 this._emptyCart();
3005 } else {
3006 this._createCart(state);
3007
3008 if (value === 0) {
3009 this._showRemoveMessage($itemElement.clone());
3010 } else {
3011 var $lineItem = $('[data-cart-item-key="' + key + '"]');
3012 var item = this.getItem(key, state);
3013
3014 $(selectors.quantityInput, $lineItem).focus();
3015 this._updateLiveRegion(item);
3016 }
3017 }
3018
3019 this._setCartCountBubble(state.item_count);
3020 }.bind(this)
3021 )
3022 .fail(
3023 function() {
3024 this._showCartError($itemQtyInputs);
3025 }.bind(this)
3026 );
3027 },
3028
3029 getItem: function(key, state) {
3030 return state.items.find(function(item) {
3031 return item.key === key;
3032 });
3033 },
3034
3035 _liveRegionText: function(item) {
3036 // Dummy content for live region
3037 var liveRegionText =
3038 theme.strings.update +
3039 ': [QuantityLabel]: [Quantity], [Regular] [$$] [DiscountedPrice] [$]. [PriceInformation]';
3040
3041 // Update Quantity
3042 liveRegionText = liveRegionText
3043 .replace('[QuantityLabel]', theme.strings.quantity)
3044 .replace('[Quantity]', item.quantity);
3045
3046 // Update pricing information
3047 var regularLabel = '';
3048 var regularPrice = theme.Currency.formatMoney(
3049 item.original_line_price,
3050 theme.moneyFormat
3051 );
3052 var discountLabel = '';
3053 var discountPrice = '';
3054 var discountInformation = '';
3055
3056 if (item.original_line_price > item.final_line_price) {
3057 regularLabel = theme.strings.regularTotal;
3058
3059 discountLabel = theme.strings.discountedTotal;
3060 discountPrice = theme.Currency.formatMoney(
3061 item.final_line_price,
3062 theme.moneyFormat
3063 );
3064
3065 discountInformation = theme.strings.priceColumn;
3066 }
3067
3068 liveRegionText = liveRegionText
3069 .replace('[Regular]', regularLabel)
3070 .replace('[$$]', regularPrice)
3071 .replace('[DiscountedPrice]', discountLabel)
3072 .replace('[$]', discountPrice)
3073 .replace('[PriceInformation]', discountInformation)
3074 .trim();
3075
3076 return liveRegionText;
3077 },
3078
3079 _updateLiveRegion: function(item) {
3080 var $liveRegion = $(selectors.cartStatus);
3081 $liveRegion.html(this._liveRegionText(item)).attr('aria-hidden', false);
3082
3083 // hide content from accessibility tree after announcement
3084 setTimeout(function() {
3085 $liveRegion.attr('aria-hidden', true);
3086 }, 1000);
3087 },
3088
3089 _createCart: function(state) {
3090 var cartDiscountList = this._createCartDiscountList(state);
3091
3092 $(selectors.cartLineItems, this.$container).html(
3093 this._createLineItemList(state)
3094 );
3095
3096 this.setQuantityFormControllers();
3097
3098 $(selectors.cartNote, this.$container).val(state.note);
3099
3100 if (cartDiscountList.length === 0) {
3101 $(selectors.cartDiscountWrapper, this.$container)
3102 .html('')
3103 .addClass(classes.hide);
3104 } else {
3105 $(selectors.cartDiscountWrapper, this.$container)
3106 .html(cartDiscountList)
3107 .removeClass(classes.hide);
3108 }
3109
3110 $(selectors.cartSubtotal, this.$container).html(
3111 theme.Currency.formatMoney(
3112 state.total_price,
3113 theme.moneyFormatWithCurrency
3114 )
3115 );
3116 },
3117
3118 _createCartDiscountList: function(cart) {
3119 return $.map(
3120 cart.cart_level_discount_applications,
3121 function(discount) {
3122 var $discount = this.$cartDiscountTemplate.clone();
3123 $discount.find(selectors.cartDiscountTitle).text(discount.title);
3124 $discount
3125 .find(selectors.cartDiscountAmount)
3126 .html(
3127 theme.Currency.formatMoney(
3128 discount.total_allocated_amount,
3129 theme.moneyFormat
3130 )
3131 );
3132 return $discount[0];
3133 }.bind(this)
3134 );
3135 },
3136
3137 _createLineItemList: function(state) {
3138 return $.map(
3139 state.items,
3140 function(item, index) {
3141 var $item = this.$itemTemplate.clone();
3142 var $itemPriceList = this.$itemPriceListTemplate.clone();
3143
3144 this._setLineItemAttributes($item, item, index);
3145 this._setLineItemImage($item, item.featured_image);
3146
3147 $(selectors.cartItemTitle, $item)
3148 .text(item.product_title)
3149 .attr('href', item.url);
3150
3151 var productDetailsList = this._createProductDetailsList(
3152 item.product_has_only_default_variant,
3153 item.options_with_values,
3154 item.properties
3155 );
3156 this._setProductDetailsList($item, productDetailsList);
3157
3158 this._setItemRemove($item, item.title);
3159
3160 $itemPriceList.html(
3161 this._createItemPrice(
3162 item.original_price,
3163 item.final_price,
3164 this.$itemPriceListTemplate
3165 )
3166 );
3167
3168 if (item.unit_price_measurement) {
3169 $itemPriceList.append(
3170 this._createUnitPrice(
3171 item.unit_price,
3172 item.unit_price_measurement,
3173 this.$itemPriceListTemplate
3174 )
3175 );
3176 }
3177
3178 this._setItemPrice($item, $itemPriceList);
3179
3180 var itemDiscountList = this._createItemDiscountList(item);
3181 this._setItemDiscountList($item, itemDiscountList);
3182
3183 this._setQuantityInputs($item, item, index);
3184
3185 var itemLinePrice = this._createItemPrice(
3186 item.original_line_price,
3187 item.final_line_price,
3188 this.$itemLinePriceTemplate
3189 );
3190 this._setItemLinePrice($item, itemLinePrice);
3191
3192 return $item[0];
3193 }.bind(this)
3194 );
3195 },
3196
3197 _setLineItemAttributes: function($item, item, index) {
3198 $item
3199 .attr(attributes.cartItemKey, item.key)
3200 .attr(attributes.cartItemUrl, item.url)
3201 .attr(attributes.cartItemTitle, item.title)
3202 .attr(attributes.cartItemIndex, index + 1)
3203 .attr(attributes.cartItemQuantity, item.quantity);
3204 },
3205
3206 _setLineItemImage: function($item, featuredImage) {
3207 var $image = $(selectors.cartItemImage, $item);
3208
3209 var sizedImageUrl =
3210 featuredImage.url !== null
3211 ? theme.Images.getSizedImageUrl(featuredImage.url, 'x190')
3212 : null;
3213
3214 if (sizedImageUrl) {
3215 $image
3216 .attr('alt', featuredImage.alt)
3217 .attr('src', sizedImageUrl)
3218 .removeClass(classes.hide);
3219 } else {
3220 $image.remove();
3221 }
3222 },
3223
3224 _setProductDetailsList: function($item, productDetailsList) {
3225 var $itemDetails = $(selectors.cartItemDetails, $item);
3226
3227 if (productDetailsList.length === 0) {
3228 $itemDetails.addClass(classes.hide).text('');
3229 } else {
3230 $itemDetails.removeClass(classes.hide).html(productDetailsList);
3231 }
3232 },
3233
3234 _setItemPrice: function($item, price) {
3235 $(selectors.cartItemPrice, $item).html(price);
3236 },
3237
3238 _setItemDiscountList: function($item, discountList) {
3239 var $itemDiscountList = $(selectors.cartItemDiscountList, $item);
3240
3241 if (discountList.length === 0) {
3242 $itemDiscountList.html('').addClass(classes.hide);
3243 } else {
3244 $itemDiscountList.html(discountList).removeClass(classes.hide);
3245 }
3246 },
3247
3248 _setItemRemove: function($item, title) {
3249 $(selectors.cartRemove, $item).attr(
3250 'aria-label',
3251 theme.strings.removeLabel.replace('[product]', title)
3252 );
3253 },
3254
3255 _setQuantityInputs: function($item, item, index) {
3256 $(selectors.quantityInputMobile, $item)
3257 .attr('id', 'updates_' + item.key)
3258 .attr(attributes.quantityItem, index + 1)
3259 .val(item.quantity);
3260
3261 $(selectors.quantityInputDesktop, $item)
3262 .attr('id', 'updates_large_' + item.key)
3263 .attr(attributes.quantityItem, index + 1)
3264 .val(item.quantity);
3265
3266 $(selectors.quantityLabelMobile, $item).attr(
3267 'for',
3268 'updates_' + item.key
3269 );
3270
3271 $(selectors.quantityLabelDesktop, $item).attr(
3272 'for',
3273 'updates_large_' + item.key
3274 );
3275 },
3276
3277 setQuantityFormControllers: function() {
3278 if (this.mql.matches) {
3279 $(selectors.quantityInputDesktop).attr('name', 'updates[]');
3280 $(selectors.quantityInputMobile).removeAttr('name');
3281 } else {
3282 $(selectors.quantityInputMobile).attr('name', 'updates[]');
3283 $(selectors.quantityInputDesktop).removeAttr('name');
3284 }
3285 },
3286
3287 _setItemLinePrice: function($item, price) {
3288 $(selectors.cartItemLinePrice, $item).html(price);
3289 },
3290
3291 _createProductDetailsList: function(
3292 product_has_only_default_variant,
3293 options,
3294 properties
3295 ) {
3296 var optionsPropertiesHTML = [];
3297
3298 if (!product_has_only_default_variant) {
3299 optionsPropertiesHTML = optionsPropertiesHTML.concat(
3300 this._getOptionList(options)
3301 );
3302 }
3303
3304 if (properties !== null && Object.keys(properties).length !== 0) {
3305 optionsPropertiesHTML = optionsPropertiesHTML.concat(
3306 this._getPropertyList(properties)
3307 );
3308 }
3309
3310 return optionsPropertiesHTML;
3311 },
3312
3313 _getOptionList: function(options) {
3314 return $.map(
3315 options,
3316 function(option) {
3317 var $optionElement = this.$itemOptionTemplate.clone();
3318
3319 $optionElement
3320 .text(option.name + ': ' + option.value)
3321 .removeClass(classes.hide);
3322
3323 return $optionElement[0];
3324 }.bind(this)
3325 );
3326 },
3327
3328 _getPropertyList: function(properties) {
3329 var propertiesArray =
3330 properties !== null ? Object.entries(properties) : [];
3331
3332 return $.map(
3333 propertiesArray,
3334 function(property) {
3335 var $propertyElement = this.$itemPropertyTemplate.clone();
3336
3337 // Line item properties prefixed with an underscore are not to be displayed
3338 if (property[0].charAt(0) === '_') return;
3339
3340 // if the property value has a length of 0 (empty), don't display it
3341 if (property[1].length === 0) return;
3342
3343 $propertyElement
3344 .find(selectors.cartItemPropertyName)
3345 .text(property[0]);
3346
3347 if (property[0].indexOf('/uploads/') === -1) {
3348 $propertyElement
3349 .find(selectors.cartItemPropertyValue)
3350 .text(': ' + property[1]);
3351 } else {
3352 $propertyElement
3353 .find(selectors.cartItemPropertyValue)
3354 .html(
3355 ': <a href="' +
3356 property[1] +
3357 '"> ' +
3358 property[1].split('/').pop() +
3359 '</a>'
3360 );
3361 }
3362
3363 $propertyElement.removeClass(classes.hide);
3364
3365 return $propertyElement[0];
3366 }.bind(this)
3367 );
3368 },
3369
3370 _createItemPrice: function(original_price, final_price, $priceTemplate) {
3371 if (original_price !== final_price) {
3372 var $discountedPrice = $(
3373 selectors.cartItemDiscountedPriceGroup,
3374 $priceTemplate
3375 ).clone();
3376
3377 $(selectors.cartItemOriginalPrice, $discountedPrice).html(
3378 theme.Currency.formatMoney(original_price, theme.moneyFormat)
3379 );
3380 $(selectors.cartItemFinalPrice, $discountedPrice).html(
3381 theme.Currency.formatMoney(final_price, theme.moneyFormat)
3382 );
3383 $discountedPrice.removeClass(classes.hide);
3384
3385 return $discountedPrice[0];
3386 } else {
3387 var $regularPrice = $(
3388 selectors.cartItemRegularPriceGroup,
3389 $priceTemplate
3390 ).clone();
3391
3392 $(selectors.cartItemRegularPrice, $regularPrice).html(
3393 theme.Currency.formatMoney(original_price, theme.moneyFormat)
3394 );
3395
3396 $regularPrice.removeClass(classes.hide);
3397
3398 return $regularPrice[0];
3399 }
3400 },
3401
3402 _createUnitPrice: function(
3403 unitPrice,
3404 unitPriceMeasurement,
3405 $itemPriceGroup
3406 ) {
3407 var $unitPriceGroup = $(
3408 selectors.unitPriceGroup,
3409 $itemPriceGroup
3410 ).clone();
3411
3412 var unitPriceBaseUnit =
3413 (unitPriceMeasurement.reference_value !== 1
3414 ? unitPriceMeasurement.reference_value
3415 : '') + unitPriceMeasurement.reference_unit;
3416
3417 $(selectors.unitPriceBaseUnit, $unitPriceGroup).text(unitPriceBaseUnit);
3418 $(selectors.unitPrice, $unitPriceGroup).html(
3419 theme.Currency.formatMoney(unitPrice, theme.moneyFormat)
3420 );
3421
3422 $unitPriceGroup.removeClass(classes.hide);
3423
3424 return $unitPriceGroup[0];
3425 },
3426
3427 _createItemDiscountList: function(item) {
3428 return $.map(
3429 item.line_level_discount_allocations,
3430 function(discount) {
3431 var $discount = this.$itemDiscountTemplate.clone();
3432 $discount
3433 .find(selectors.cartItemDiscountTitle)
3434 .text(discount.discount_application.title);
3435 $discount
3436 .find(selectors.cartItemDiscountAmount)
3437 .html(
3438 theme.Currency.formatMoney(discount.amount, theme.moneyFormat)
3439 );
3440 return $discount[0];
3441 }.bind(this)
3442 );
3443 },
3444
3445 _showQuantityErrorMessages: function(itemElement) {
3446 $(selectors.cartQuantityErrorMessage, itemElement).text(
3447 theme.strings.quantityMinimumMessage
3448 );
3449
3450 $(selectors.cartQuantityErrorMessageWrapper, itemElement).removeClass(
3451 classes.hide
3452 );
3453
3454 $(selectors.inputQty, itemElement)
3455 .addClass(classes.inputError)
3456 .focus();
3457 },
3458
3459 _hideQuantityErrorMessage: function() {
3460 var $errorMessages = $(
3461 selectors.cartQuantityErrorMessageWrapper
3462 ).addClass(classes.hide);
3463
3464 $(selectors.cartQuantityErrorMessage, $errorMessages).text('');
3465
3466 $(selectors.inputQty, this.$container).removeClass(classes.inputError);
3467 },
3468
3469 _handleThumbnailClick: function(evt) {
3470 var url = $(evt.target)
3471 .closest(selectors.cartItem)
3472 .data('cart-item-url');
3473
3474 window.location.href = url;
3475 },
3476
3477 _onNoteChange: function(evt) {
3478 var note = evt.currentTarget.value;
3479 this._hideCartError();
3480 this._hideQuantityErrorMessage();
3481
3482 var params = {
3483 url: '/cart/update.js',
3484 data: { note: note },
3485 dataType: 'json'
3486 };
3487
3488 $.post(params).fail(
3489 function() {
3490 this._showCartError(evt.currentTarget);
3491 }.bind(this)
3492 );
3493 },
3494
3495 _showCartError: function(elementToFocus) {
3496 $(selectors.cartErrorMessage).text(theme.strings.cartError);
3497
3498 $(selectors.cartErrorMessageWrapper).removeClass(classes.hide);
3499
3500 elementToFocus.focus();
3501 },
3502
3503 _hideCartError: function() {
3504 $(selectors.cartErrorMessageWrapper).addClass(classes.hide);
3505 $(selectors.cartErrorMessage).text('');
3506 },
3507
3508 _onRemoveItem: function(evt) {
3509 evt.preventDefault();
3510 var $remove = $(evt.target);
3511 var $lineItem = $remove.closest(selectors.cartItem);
3512 var index = $lineItem.attr(attributes.cartItemIndex);
3513 this._hideCartError();
3514
3515 var params = {
3516 url: '/cart/change.js',
3517 data: { quantity: 0, line: index },
3518 dataType: 'json'
3519 };
3520
3521 $.post(params)
3522 .done(
3523 function(state) {
3524 if (state.item_count === 0) {
3525 this._emptyCart();
3526 } else {
3527 this._createCart(state);
3528 this._showRemoveMessage($lineItem.clone());
3529 }
3530
3531 this._setCartCountBubble(state.item_count);
3532 }.bind(this)
3533 )
3534 .fail(
3535 function() {
3536 this._showCartError(null);
3537 }.bind(this)
3538 );
3539 },
3540
3541 _showRemoveMessage: function(lineItem) {
3542 var index = lineItem.data('cart-item-index');
3543 var removeMessage = this._getRemoveMessage(lineItem);
3544 var $lineItemAtIndex;
3545
3546 if (index - 1 === 0) {
3547 $lineItemAtIndex = $('[data-cart-item-index="1"]', this.$container);
3548 $(removeMessage).insertBefore($lineItemAtIndex);
3549 } else {
3550 $lineItemAtIndex = $(
3551 '[data-cart-item-index="' + (index - 1) + '"]',
3552 this.$container
3553 );
3554 removeMessage.insertAfter($lineItemAtIndex);
3555 }
3556 removeMessage.focus();
3557 },
3558
3559 _getRemoveMessage: function(lineItem) {
3560 var formattedMessage = this._formatRemoveMessage(lineItem);
3561
3562 var $tableCell = $(selectors.cartTableCell, lineItem).clone();
3563 $tableCell
3564 .removeClass()
3565 .addClass(classes.cartRemovedProduct)
3566 .attr('colspan', '4')
3567 .html(formattedMessage);
3568
3569 lineItem
3570 .attr('role', 'alert')
3571 .html($tableCell)
3572 .attr('tabindex', '-1');
3573
3574 return lineItem;
3575 },
3576
3577 _formatRemoveMessage: function(lineItem) {
3578 var quantity = lineItem.data('cart-item-quantity');
3579 var url = lineItem.attr(attributes.cartItemUrl);
3580 var title = lineItem.attr(attributes.cartItemTitle);
3581
3582 return theme.strings.removedItemMessage
3583 .replace('[quantity]', quantity)
3584 .replace(
3585 '[link]',
3586 '<a ' +
3587 'href="' +
3588 url +
3589 '" class="text-link text-link--accent">' +
3590 title +
3591 '</a>'
3592 );
3593 },
3594
3595 _setCartCountBubble: function(quantity) {
3596 this.$cartCountBubble =
3597 this.$cartCountBubble || $(selectors.cartCountBubble);
3598 this.$cartCount = this.$cartCount || $(selectors.cartCount);
3599
3600 if (quantity > 0) {
3601 this.$cartCountBubble.removeClass(classes.hide);
3602 this.$cartCount.html(quantity);
3603 } else {
3604 this.$cartCountBubble.addClass(classes.hide);
3605 this.$cartCount.html('');
3606 }
3607 },
3608
3609 _emptyCart: function() {
3610 this.$emptyPageContent =
3611 this.$emptyPageContent ||
3612 $(selectors.emptyPageContent, this.$container);
3613 this.$cartWrapper =
3614 this.$cartWrapper || $(selectors.cartWrapper, this.$container);
3615
3616 this.$emptyPageContent.removeClass(classes.hide);
3617 this.$cartWrapper.addClass(classes.hide);
3618 },
3619
3620 cookiesEnabled: function() {
3621 var cookieEnabled = navigator.cookieEnabled;
3622
3623 if (!cookieEnabled) {
3624 document.cookie = 'testcookie';
3625 cookieEnabled = document.cookie.indexOf('testcookie') !== -1;
3626 }
3627 return cookieEnabled;
3628 }
3629 });
3630
3631 return Cart;
3632})();
3633
3634window.theme = window.theme || {};
3635
3636theme.Filters = (function() {
3637 var settings = {
3638 // Breakpoints from src/stylesheets/global/variables.scss.liquid
3639 mediaQueryMediumUp: 'screen and (min-width: 750px)'
3640 };
3641
3642 var constants = {
3643 SORT_BY: 'sort_by'
3644 };
3645
3646 var selectors = {
3647 mainContent: '#MainContent',
3648 filterSelection: '#FilterTags',
3649 sortSelection: '#SortBy'
3650 };
3651
3652 var data = {
3653 sortBy: 'data-default-sortby'
3654 };
3655
3656 function Filters(container) {
3657 var $container = (this.$container = $(container));
3658
3659 this.$filterSelect = $(selectors.filterSelection, $container);
3660 this.$sortSelect = $(selectors.sortSelection, $container);
3661 this.$selects = $(selectors.filterSelection, $container).add(
3662 $(selectors.sortSelection, $container)
3663 );
3664
3665 this.defaultSort = this._getDefaultSortValue();
3666 this.$selects.removeClass('hidden');
3667
3668 this.$filterSelect.on('change', this._onFilterChange.bind(this));
3669 this.$sortSelect.on('change', this._onSortChange.bind(this));
3670 this._initBreakpoints();
3671 }
3672
3673 Filters.prototype = _.assignIn({}, Filters.prototype, {
3674 _initBreakpoints: function() {
3675 var self = this;
3676
3677 enquire.register(settings.mediaQueryMediumUp, {
3678 match: function() {
3679 self._resizeSelect(self.$selects);
3680 }
3681 });
3682 },
3683
3684 _onSortChange: function() {
3685 var sort = this._sortValue();
3686 var url = window.location.href.replace(window.location.search, '');
3687 var queryStringValue = slate.utils.getParameterByName('q');
3688 var query = queryStringValue !== null ? queryStringValue : '';
3689
3690 if (sort.length) {
3691 var urlStripped = url.replace(window.location.hash, '');
3692 query = query !== '' ? '?q=' + query + '&' : '?';
3693
3694 window.location.href =
3695 urlStripped + query + sort + selectors.mainContent;
3696 } else {
3697 // clean up our url if the sort value is blank for default
3698 window.location.href = url;
3699 }
3700 },
3701
3702 _onFilterChange: function() {
3703 var filter = this._getFilterValue();
3704
3705 // remove the 'page' parameter to go to the first page of results
3706 var search = document.location.search.replace(/\?(page=\w+)?&?/, '');
3707
3708 // only add the search parameters to the url if they exist
3709 search = search !== '' ? '?' + search : '';
3710
3711 document.location.href = filter + search + selectors.mainContent;
3712 },
3713
3714 _getFilterValue: function() {
3715 return this.$filterSelect.val();
3716 },
3717
3718 _getSortValue: function() {
3719 return this.$sortSelect.val() || this.defaultSort;
3720 },
3721
3722 _getDefaultSortValue: function() {
3723 return this.$sortSelect.attr(data.sortBy);
3724 },
3725
3726 _sortValue: function() {
3727 var sort = this._getSortValue();
3728 var query = '';
3729
3730 if (sort !== this.defaultSort) {
3731 query = constants.SORT_BY + '=' + sort;
3732 }
3733
3734 return query;
3735 },
3736
3737 _resizeSelect: function($selection) {
3738 $selection.each(function() {
3739 var $this = $(this);
3740 var arrowWidth = 10;
3741 // create test element
3742 var text = $this.find('option:selected').text();
3743 var $test = $('<span>').html(text);
3744
3745 // add to body, get width, and get out
3746 $test.appendTo('body');
3747 var width = $test.width();
3748 $test.remove();
3749
3750 // set select width
3751 $this.width(width + arrowWidth);
3752 });
3753 },
3754
3755 onUnload: function() {
3756 this.$filterSelect.off('change', this._onFilterChange);
3757 this.$sortSelect.off('change', this._onSortChange);
3758 }
3759 });
3760
3761 return Filters;
3762})();
3763
3764window.theme = window.theme || {};
3765
3766theme.HeaderSection = (function() {
3767 function Header() {
3768 theme.Header.init();
3769 theme.MobileNav.init();
3770 theme.Search.init();
3771 }
3772
3773 Header.prototype = _.assignIn({}, Header.prototype, {
3774 onUnload: function() {
3775 theme.Header.unload();
3776 }
3777 });
3778
3779 return Header;
3780})();
3781
3782theme.Maps = (function() {
3783 var config = {
3784 zoom: 14
3785 };
3786 var apiStatus = null;
3787 var mapsToLoad = [];
3788
3789 var errors = {
3790 addressNoResults: theme.strings.addressNoResults,
3791 addressQueryLimit: theme.strings.addressQueryLimit,
3792 addressError: theme.strings.addressError,
3793 authError: theme.strings.authError
3794 };
3795
3796 var selectors = {
3797 section: '[data-section-type="map"]',
3798 map: '[data-map]',
3799 mapOverlay: '[data-map-overlay]'
3800 };
3801
3802 var classes = {
3803 mapError: 'map-section--load-error',
3804 errorMsg: 'map-section__error errors text-center'
3805 };
3806
3807 // Global function called by Google on auth errors.
3808 // Show an auto error message on all map instances.
3809 // eslint-disable-next-line camelcase, no-unused-vars
3810 window.gm_authFailure = function() {
3811 if (!Shopify.designMode) {
3812 return;
3813 }
3814
3815 $(selectors.section).addClass(classes.mapError);
3816 $(selectors.map).remove();
3817 $(selectors.mapOverlay).after(
3818 '<div class="' +
3819 classes.errorMsg +
3820 '">' +
3821 theme.strings.authError +
3822 '</div>'
3823 );
3824 };
3825
3826 function Map(container) {
3827 this.$container = $(container);
3828 this.$map = this.$container.find(selectors.map);
3829 this.key = this.$map.data('api-key');
3830
3831 if (typeof this.key === 'undefined') {
3832 return;
3833 }
3834
3835 if (apiStatus === 'loaded') {
3836 this.createMap();
3837 } else {
3838 mapsToLoad.push(this);
3839
3840 if (apiStatus !== 'loading') {
3841 apiStatus = 'loading';
3842 if (typeof window.google === 'undefined') {
3843 $.getScript(
3844 'https://maps.googleapis.com/maps/api/js?key=' + this.key
3845 ).then(function() {
3846 apiStatus = 'loaded';
3847 initAllMaps();
3848 });
3849 }
3850 }
3851 }
3852 }
3853
3854 function initAllMaps() {
3855 // API has loaded, load all Map instances in queue
3856 $.each(mapsToLoad, function(index, instance) {
3857 instance.createMap();
3858 });
3859 }
3860
3861 function geolocate($map) {
3862 var deferred = $.Deferred();
3863 var geocoder = new google.maps.Geocoder();
3864 var address = $map.data('address-setting');
3865
3866 geocoder.geocode({ address: address }, function(results, status) {
3867 if (status !== google.maps.GeocoderStatus.OK) {
3868 deferred.reject(status);
3869 }
3870
3871 deferred.resolve(results);
3872 });
3873
3874 return deferred;
3875 }
3876
3877 Map.prototype = _.assignIn({}, Map.prototype, {
3878 createMap: function() {
3879 var $map = this.$map;
3880
3881 return geolocate($map)
3882 .then(
3883 function(results) {
3884 var mapOptions = {
3885 zoom: config.zoom,
3886 center: results[0].geometry.location,
3887 draggable: false,
3888 clickableIcons: false,
3889 scrollwheel: false,
3890 disableDoubleClickZoom: true,
3891 disableDefaultUI: true
3892 };
3893
3894 var map = (this.map = new google.maps.Map($map[0], mapOptions));
3895 var center = (this.center = map.getCenter());
3896
3897 //eslint-disable-next-line no-unused-vars
3898 var marker = new google.maps.Marker({
3899 map: map,
3900 position: map.getCenter()
3901 });
3902
3903 google.maps.event.addDomListener(
3904 window,
3905 'resize',
3906 $.debounce(250, function() {
3907 google.maps.event.trigger(map, 'resize');
3908 map.setCenter(center);
3909 $map.removeAttr('style');
3910 })
3911 );
3912 }.bind(this)
3913 )
3914 .fail(function() {
3915 var errorMessage;
3916
3917 switch (status) {
3918 case 'ZERO_RESULTS':
3919 errorMessage = errors.addressNoResults;
3920 break;
3921 case 'OVER_QUERY_LIMIT':
3922 errorMessage = errors.addressQueryLimit;
3923 break;
3924 case 'REQUEST_DENIED':
3925 errorMessage = errors.authError;
3926 break;
3927 default:
3928 errorMessage = errors.addressError;
3929 break;
3930 }
3931
3932 // Show errors only to merchant in the editor.
3933 if (Shopify.designMode) {
3934 $map
3935 .parent()
3936 .addClass(classes.mapError)
3937 .html(
3938 '<div class="' +
3939 classes.errorMsg +
3940 '">' +
3941 errorMessage +
3942 '</div>'
3943 );
3944 }
3945 });
3946 },
3947
3948 onUnload: function() {
3949 if (this.$map.length === 0) {
3950 return;
3951 }
3952 google.maps.event.clearListeners(this.map, 'resize');
3953 }
3954 });
3955
3956 return Map;
3957})();
3958
3959/* eslint-disable no-new */
3960theme.Product = (function() {
3961 function Product(container) {
3962 var $container = (this.$container = $(container));
3963 var sectionId = $container.attr('data-section-id');
3964 this.ajaxEnabled = $container.data('ajax-enabled');
3965
3966 this.settings = {
3967 // Breakpoints from src/stylesheets/global/variables.scss.liquid
3968 mediaQueryMediumUp: 'screen and (min-width: 750px)',
3969 mediaQuerySmall: 'screen and (max-width: 749px)',
3970 bpSmall: false,
3971 enableHistoryState: $container.data('enable-history-state') || false,
3972 namespace: '.slideshow-' + sectionId,
3973 sectionId: sectionId,
3974 sliderActive: false,
3975 zoomEnabled: false
3976 };
3977
3978 this.selectors = {
3979 addToCart: '[data-add-to-cart]',
3980 addToCartText: '[data-add-to-cart-text]',
3981 cartCount: '[data-cart-count]',
3982 cartCountBubble: '[data-cart-count-bubble]',
3983 cartPopup: '[data-cart-popup]',
3984 cartPopupCartQuantity: '[data-cart-popup-cart-quantity]',
3985 cartPopupClose: '[data-cart-popup-close]',
3986 cartPopupDismiss: '[data-cart-popup-dismiss]',
3987 cartPopupImage: '[data-cart-popup-image]',
3988 cartPopupImageWrapper: '[data-cart-popup-image-wrapper]',
3989 cartPopupImagePlaceholder: '[data-cart-popup-image-placeholder]',
3990 cartPopupPlaceholderSize: '[data-placeholder-size]',
3991 cartPopupProductDetails: '[data-cart-popup-product-details]',
3992 cartPopupQuantity: '[data-cart-popup-quantity]',
3993 cartPopupQuantityLabel: '[data-cart-popup-quantity-label]',
3994 cartPopupTitle: '[data-cart-popup-title]',
3995 cartPopupWrapper: '[data-cart-popup-wrapper]',
3996 loader: '[data-loader]',
3997 loaderStatus: '[data-loader-status]',
3998 quantity: '[data-quantity-input]',
3999 SKU: '.variant-sku',
4000 productStatus: '[data-product-status]',
4001 originalSelectorId: '#ProductSelect-' + sectionId,
4002 productForm: '[data-product-form]',
4003 errorMessage: '[data-error-message]',
4004 errorMessageWrapper: '[data-error-message-wrapper]',
4005 productImageWraps: '.product-single__photo',
4006 productThumbImages: '.product-single__thumbnail--' + sectionId,
4007 productThumbs: '.product-single__thumbnails-' + sectionId,
4008 productThumbListItem: '.product-single__thumbnails-item',
4009 productFeaturedImage: '.product-featured-img',
4010 productThumbsWrapper: '.thumbnails-wrapper',
4011 saleLabel: '.product-price__sale-label-' + sectionId,
4012 singleOptionSelector: '.single-option-selector-' + sectionId,
4013 shopifyPaymentButton: '.shopify-payment-button',
4014 priceContainer: '[data-price]',
4015 regularPrice: '[data-regular-price]',
4016 salePrice: '[data-sale-price]',
4017 unitPrice: '[data-unit-price]',
4018 unitPriceBaseUnit: '[data-unit-price-base-unit]'
4019 };
4020
4021 this.classes = {
4022 cartPopupWrapperHidden: 'cart-popup-wrapper--hidden',
4023 hidden: 'hide',
4024 inputError: 'input--error',
4025 productOnSale: 'price--on-sale',
4026 productUnitAvailable: 'price--unit-available',
4027 productUnavailable: 'price--unavailable',
4028 cartImage: 'cart-popup-item__image',
4029 productFormErrorMessageWrapperHidden:
4030 'product-form__error-message-wrapper--hidden',
4031 activeClass: 'active-thumb'
4032 };
4033
4034 this.$quantityInput = $(this.selectors.quantity, $container);
4035 this.$errorMessageWrapper = $(
4036 this.selectors.errorMessageWrapper,
4037 $container
4038 );
4039 this.$addToCart = $(this.selectors.addToCart, $container);
4040 this.$addToCartText = $(this.selectors.addToCartText, this.$addToCart);
4041 this.$shopifyPaymentButton = $(
4042 this.selectors.shopifyPaymentButton,
4043 $container
4044 );
4045
4046 this.$loader = $(this.selectors.loader, this.$addToCart);
4047 this.$loaderStatus = $(this.selectors.loaderStatus, $container);
4048
4049 // Stop parsing if we don't have the product json script tag when loading
4050 // section in the Theme Editor
4051 if (!$('#ProductJson-' + sectionId).html()) {
4052 return;
4053 }
4054
4055 this.productSingleObject = JSON.parse(
4056 document.getElementById('ProductJson-' + sectionId).innerHTML
4057 );
4058
4059 this.settings.zoomEnabled = $(this.selectors.productImageWraps).hasClass(
4060 'js-zoom-enabled'
4061 );
4062
4063 this._initBreakpoints();
4064 this._stringOverrides();
4065 this._initVariants();
4066 this._initImageSwitch();
4067 this._initAddToCart();
4068 this._setActiveThumbnail();
4069 }
4070
4071 Product.prototype = _.assignIn({}, Product.prototype, {
4072 _stringOverrides: function() {
4073 theme.productStrings = theme.productStrings || {};
4074 $.extend(theme.strings, theme.productStrings);
4075 },
4076
4077 _initBreakpoints: function() {
4078 var self = this;
4079
4080 enquire.register(this.settings.mediaQuerySmall, {
4081 match: function() {
4082 // initialize thumbnail slider on mobile if more than three thumbnails
4083 if ($(self.selectors.productThumbImages).length > 3) {
4084 self._initThumbnailSlider();
4085 }
4086
4087 // destroy image zooming if enabled
4088 if (self.settings.zoomEnabled) {
4089 $(self.selectors.productImageWraps).each(function() {
4090 _destroyZoom(this);
4091 });
4092 }
4093
4094 self.settings.bpSmall = true;
4095 },
4096 unmatch: function() {
4097 if (self.settings.sliderActive) {
4098 self._destroyThumbnailSlider();
4099 }
4100
4101 self.settings.bpSmall = false;
4102 }
4103 });
4104
4105 enquire.register(this.settings.mediaQueryMediumUp, {
4106 match: function() {
4107 if (self.settings.zoomEnabled) {
4108 $(self.selectors.productImageWraps).each(function() {
4109 _enableZoom(this);
4110 });
4111 }
4112 }
4113 });
4114 },
4115
4116 _initVariants: function() {
4117 var options = {
4118 $container: this.$container,
4119 enableHistoryState:
4120 this.$container.data('enable-history-state') || false,
4121 singleOptionSelector: this.selectors.singleOptionSelector,
4122 originalSelectorId: this.selectors.originalSelectorId,
4123 product: this.productSingleObject
4124 };
4125
4126 this.variants = new slate.Variants(options);
4127
4128 this.$container.on(
4129 'variantChange' + this.settings.namespace,
4130 this._updateAvailability.bind(this)
4131 );
4132 this.$container.on(
4133 'variantImageChange' + this.settings.namespace,
4134 this._updateImages.bind(this)
4135 );
4136 this.$container.on(
4137 'variantPriceChange' + this.settings.namespace,
4138 this._updatePrice.bind(this)
4139 );
4140 this.$container.on(
4141 'variantSKUChange' + this.settings.namespace,
4142 this._updateSKU.bind(this)
4143 );
4144 },
4145
4146 _initImageSwitch: function() {
4147 if (!$(this.selectors.productThumbImages).length) {
4148 return;
4149 }
4150
4151 var self = this;
4152
4153 $(this.selectors.productThumbImages)
4154 .on('click', function(evt) {
4155 evt.preventDefault();
4156 var $el = $(this);
4157
4158 var imageId = $el.data('thumbnail-id');
4159
4160 self._switchImage(imageId);
4161 self._setActiveThumbnail(imageId);
4162 })
4163 .on('keyup', self._handleImageFocus.bind(self));
4164 },
4165
4166 _initAddToCart: function() {
4167 $(this.selectors.productForm, this.$container).on(
4168 'submit',
4169 function(evt) {
4170 if (this.$addToCart.is('[aria-disabled]')) {
4171 evt.preventDefault();
4172 return;
4173 }
4174
4175 if (!this.ajaxEnabled) return;
4176
4177 evt.preventDefault();
4178
4179 this.$previouslyFocusedElement = $(':focus');
4180
4181 var isInvalidQuantity = this.$quantityInput.val() <= 0;
4182
4183 if (isInvalidQuantity) {
4184 this._showErrorMessage(theme.strings.quantityMinimumMessage);
4185 return;
4186 }
4187
4188 if (!isInvalidQuantity && this.ajaxEnabled) {
4189 // disable the addToCart and dynamic checkout button while
4190 // request/cart popup is loading and handle loading state
4191 this._handleButtonLoadingState(true);
4192 var $data = $(this.selectors.productForm, this.$container);
4193 this._addItemToCart($data);
4194 return;
4195 }
4196 }.bind(this)
4197 );
4198 },
4199
4200 _addItemToCart: function(data) {
4201 var params = {
4202 url: '/cart/add.js',
4203 data: $(data).serialize(),
4204 dataType: 'json'
4205 };
4206
4207 $.post(params)
4208 .done(
4209 function(item) {
4210 this._hideErrorMessage();
4211 this._setupCartPopup(item);
4212 }.bind(this)
4213 )
4214 .fail(
4215 function(response) {
4216 this.$previouslyFocusedElement.focus();
4217 var errorMessage = response.responseJSON
4218 ? response.responseJSON.description
4219 : theme.strings.cartError;
4220 this._showErrorMessage(errorMessage);
4221 this._handleButtonLoadingState(false);
4222 }.bind(this)
4223 );
4224 },
4225
4226 _handleButtonLoadingState: function(isLoading) {
4227 if (isLoading) {
4228 this.$addToCart.attr('aria-disabled', true);
4229 this.$addToCartText.addClass(this.classes.hidden);
4230 this.$loader.removeClass(this.classes.hidden);
4231 this.$shopifyPaymentButton.attr('disabled', true);
4232 this.$loaderStatus.attr('aria-hidden', false);
4233 } else {
4234 this.$addToCart.removeAttr('aria-disabled');
4235 this.$addToCartText.removeClass(this.classes.hidden);
4236 this.$loader.addClass(this.classes.hidden);
4237 this.$shopifyPaymentButton.removeAttr('disabled');
4238 this.$loaderStatus.attr('aria-hidden', true);
4239 }
4240 },
4241
4242 _showErrorMessage: function(errorMessage) {
4243 $(this.selectors.errorMessage, this.$container).html(errorMessage);
4244
4245 if (this.$quantityInput.length !== 0) {
4246 this.$quantityInput.addClass(this.classes.inputError);
4247 }
4248
4249 this.$errorMessageWrapper
4250 .removeClass(this.classes.productFormErrorMessageWrapperHidden)
4251 .attr('aria-hidden', true)
4252 .removeAttr('aria-hidden');
4253 },
4254
4255 _hideErrorMessage: function() {
4256 this.$errorMessageWrapper.addClass(
4257 this.classes.productFormErrorMessageWrapperHidden
4258 );
4259
4260 if (this.$quantityInput.length !== 0) {
4261 this.$quantityInput.removeClass(this.classes.inputError);
4262 }
4263 },
4264
4265 _setupCartPopup: function(item) {
4266 this.$cartPopup = this.$cartPopup || $(this.selectors.cartPopup);
4267 this.$cartPopupWrapper =
4268 this.$cartPopupWrapper || $(this.selectors.cartPopupWrapper);
4269 this.$cartPopupTitle =
4270 this.$cartPopupTitle || $(this.selectors.cartPopupTitle);
4271 this.$cartPopupQuantity =
4272 this.$cartPopupQuantity || $(this.selectors.cartPopupQuantity);
4273 this.$cartPopupQuantityLabel =
4274 this.$cartPopupQuantityLabel ||
4275 $(this.selectors.cartPopupQuantityLabel);
4276 this.$cartPopupClose =
4277 this.$cartPopupClose || $(this.selectors.cartPopupClose);
4278 this.$cartPopupDismiss =
4279 this.cartPopupDismiss || $(this.selectors.cartPopupDismiss);
4280 this.$cartPopupImagePlaceholder =
4281 this.$cartPopupImagePlaceholder ||
4282 $(this.selectors.cartPopupImagePlaceholder);
4283
4284 this._setupCartPopupEventListeners();
4285
4286 this._updateCartPopupContent(item);
4287 },
4288
4289 _updateCartPopupContent: function(item) {
4290 var quantity = this.$quantityInput.length ? this.$quantityInput.val() : 1;
4291
4292 this.$cartPopupTitle.text(item.product_title);
4293 this.$cartPopupQuantity.text(quantity);
4294 this.$cartPopupQuantityLabel.text(
4295 theme.strings.quantityLabel.replace('[count]', quantity)
4296 );
4297
4298 this._setCartPopupPlaceholder(
4299 item.featured_image.url,
4300 item.featured_image.aspect_ratio
4301 );
4302 this._setCartPopupImage(item.featured_image.url, item.featured_image.alt);
4303 this._setCartPopupProductDetails(
4304 item.product_has_only_default_variant,
4305 item.options_with_values,
4306 item.properties
4307 );
4308
4309 $.getJSON('/cart.js').then(
4310 function(cart) {
4311 this._setCartQuantity(cart.item_count);
4312 this._setCartCountBubble(cart.item_count);
4313 this._showCartPopup();
4314 }.bind(this)
4315 );
4316 },
4317
4318 _setupCartPopupEventListeners: function() {
4319 this.$cartPopupWrapper.on(
4320 'keyup',
4321 function(event) {
4322 if (event.keyCode === slate.utils.keyboardKeys.ESCAPE) {
4323 this._hideCartPopup(event);
4324 }
4325 }.bind(this)
4326 );
4327
4328 this.$cartPopupClose.on('click', this._hideCartPopup.bind(this));
4329 this.$cartPopupDismiss.on('click', this._hideCartPopup.bind(this));
4330 $('body').on('click', this._onBodyClick.bind(this));
4331 },
4332
4333 _setCartPopupPlaceholder: function(imageUrl, imageAspectRatio) {
4334 this.$cartPopupImageWrapper =
4335 this.$cartPopupImageWrapper || $(this.selectors.cartPopupImageWrapper);
4336
4337 if (imageUrl === null) {
4338 this.$cartPopupImageWrapper.addClass(this.classes.hidden);
4339 return;
4340 }
4341
4342 var $placeholder = $(this.selectors.cartPopupPlaceholderSize);
4343 var maxWidth = 95 * imageAspectRatio;
4344 var heightRatio = 100 / imageAspectRatio;
4345
4346 this.$cartPopupImagePlaceholder.css('max-width', maxWidth);
4347
4348 $placeholder.css('padding-top', heightRatio + '%');
4349 },
4350
4351 _setCartPopupImage: function(imageUrl, imageAlt) {
4352 if (imageUrl === null) return;
4353
4354 this.$cartPopupImageWrapper.removeClass(this.classes.hidden);
4355 var sizedImageUrl = theme.Images.getSizedImageUrl(imageUrl, '200x');
4356 var image = document.createElement('img');
4357 image.src = sizedImageUrl;
4358 image.alt = imageAlt;
4359 image.classList.add(this.classes.cartImage);
4360 image.dataset.cartPopupImage = '';
4361
4362 image.onload = function() {
4363 this.$cartPopupImagePlaceholder.addClass(this.classes.hidden);
4364 this.$cartPopupImageWrapper.append(image);
4365 }.bind(this);
4366 },
4367
4368 _setCartPopupProductDetails: function(
4369 product_has_only_default_variant,
4370 options,
4371 properties
4372 ) {
4373 this.$cartPopupProductDetails =
4374 this.$cartPopupProductDetails ||
4375 $(this.selectors.cartPopupProductDetails);
4376 var variantPropertiesHTML = '';
4377
4378 if (!product_has_only_default_variant) {
4379 variantPropertiesHTML =
4380 variantPropertiesHTML + this._getVariantOptionList(options);
4381 }
4382
4383 if (properties !== null && Object.keys(properties).length !== 0) {
4384 variantPropertiesHTML =
4385 variantPropertiesHTML + this._getPropertyList(properties);
4386 }
4387
4388 if (variantPropertiesHTML.length === 0) {
4389 this.$cartPopupProductDetails.html('');
4390 this.$cartPopupProductDetails.attr('hidden', '');
4391 } else {
4392 this.$cartPopupProductDetails.html(variantPropertiesHTML);
4393 this.$cartPopupProductDetails.removeAttr('hidden');
4394 }
4395 },
4396
4397 _getVariantOptionList: function(variantOptions) {
4398 var variantOptionListHTML = '';
4399
4400 variantOptions.forEach(function(variantOption) {
4401 variantOptionListHTML =
4402 variantOptionListHTML +
4403 '<li class="product-details__item product-details__item--variant-option">' +
4404 variantOption.name +
4405 ': ' +
4406 variantOption.value +
4407 '</li>';
4408 });
4409
4410 return variantOptionListHTML;
4411 },
4412
4413 _getPropertyList: function(properties) {
4414 var propertyListHTML = '';
4415 var propertiesArray = Object.entries(properties);
4416
4417 propertiesArray.forEach(function(property) {
4418 // Line item properties prefixed with an underscore are not to be displayed
4419 if (property[0].charAt(0) === '_') return;
4420
4421 // if the property value has a length of 0 (empty), don't display it
4422 if (property[1].length === 0) return;
4423
4424 propertyListHTML =
4425 propertyListHTML +
4426 '<li class="product-details__item product-details__item--property">' +
4427 '<span class="product-details__property-label">' +
4428 property[0] +
4429 ': </span>' +
4430 property[1];
4431 ': ' + '</li>';
4432 });
4433
4434 return propertyListHTML;
4435 },
4436
4437 _setCartQuantity: function(quantity) {
4438 this.$cartPopupCartQuantity =
4439 this.$cartPopupCartQuantity || $(this.selectors.cartPopupCartQuantity);
4440 var ariaLabel;
4441
4442 if (quantity === 1) {
4443 ariaLabel = theme.strings.oneCartCount;
4444 } else if (quantity > 1) {
4445 ariaLabel = theme.strings.otherCartCount.replace('[count]', quantity);
4446 }
4447
4448 this.$cartPopupCartQuantity.text(quantity).attr('aria-label', ariaLabel);
4449 },
4450
4451 _setCartCountBubble: function(quantity) {
4452 this.$cartCountBubble =
4453 this.$cartCountBubble || $(this.selectors.cartCountBubble);
4454 this.$cartCount = this.$cartCount || $(this.selectors.cartCount);
4455
4456 this.$cartCountBubble.removeClass(this.classes.hidden);
4457 this.$cartCount.text(quantity);
4458 },
4459
4460 _showCartPopup: function() {
4461 this.$cartPopupWrapper
4462 .prepareTransition()
4463 .removeClass(this.classes.cartPopupWrapperHidden);
4464 this._handleButtonLoadingState(false);
4465
4466 slate.a11y.trapFocus({
4467 $container: this.$cartPopupWrapper,
4468 $elementToFocus: this.$cartPopup,
4469 namespace: 'cartPopupFocus'
4470 });
4471 },
4472
4473 _hideCartPopup: function(event) {
4474 var setFocus = event.detail === 0 ? true : false;
4475 this.$cartPopupWrapper
4476 .prepareTransition()
4477 .addClass(this.classes.cartPopupWrapperHidden);
4478
4479 $(this.selectors.cartPopupImage).remove();
4480 this.$cartPopupImagePlaceholder.removeClass(this.classes.hidden);
4481
4482 slate.a11y.removeTrapFocus({
4483 $container: this.$cartPopupWrapper,
4484 namespace: 'cartPopupFocus'
4485 });
4486
4487 if (setFocus) this.$previouslyFocusedElement[0].focus();
4488
4489 this.$cartPopupWrapper.off('keyup');
4490 this.$cartPopupClose.off('click');
4491 this.$cartPopupDismiss.off('click');
4492 $('body').off('click');
4493 },
4494
4495 _onBodyClick: function(event) {
4496 var $target = $(event.target);
4497
4498 if (
4499 $target[0] !== this.$cartPopupWrapper[0] &&
4500 !$target.parents(this.selectors.cartPopup).length
4501 ) {
4502 this._hideCartPopup(event);
4503 }
4504 },
4505
4506 _setActiveThumbnail: function(imageId) {
4507 // If there is no element passed, find it by the current product image
4508 if (typeof imageId === 'undefined') {
4509 imageId = $(
4510 this.selectors.productImageWraps + ':not(.hide)',
4511 this.$container
4512 ).data('image-id');
4513 }
4514
4515 var $thumbnailWrappers = $(
4516 this.selectors.productThumbListItem + ':not(.slick-cloned)',
4517 this.$container
4518 );
4519
4520 var $activeThumbnail = $thumbnailWrappers.find(
4521 this.selectors.productThumbImages +
4522 "[data-thumbnail-id='" +
4523 imageId +
4524 "']"
4525 );
4526
4527 $(this.selectors.productThumbImages)
4528 .removeClass(this.classes.activeClass)
4529 .removeAttr('aria-current');
4530
4531 $activeThumbnail.addClass(this.classes.activeClass);
4532 $activeThumbnail.attr('aria-current', true);
4533
4534 if (!$thumbnailWrappers.hasClass('slick-slide')) {
4535 return;
4536 }
4537
4538 var slideIndex = $activeThumbnail.parent().data('slick-index');
4539
4540 $(this.selectors.productThumbs).slick('slickGoTo', slideIndex, true);
4541 },
4542
4543 _switchImage: function(imageId) {
4544 var $newImage = $(
4545 this.selectors.productImageWraps + "[data-image-id='" + imageId + "']",
4546 this.$container
4547 );
4548 var $otherImages = $(
4549 this.selectors.productImageWraps +
4550 ":not([data-image-id='" +
4551 imageId +
4552 "'])",
4553 this.$container
4554 );
4555
4556 $newImage.removeClass(this.classes.hidden);
4557 $otherImages.addClass(this.classes.hidden);
4558 },
4559
4560 _handleImageFocus: function(evt) {
4561 if (evt.keyCode !== slate.utils.keyboardKeys.ENTER) return;
4562
4563 $(this.selectors.productFeaturedImage + ':visible').focus();
4564 },
4565
4566 _initThumbnailSlider: function() {
4567 var options = {
4568 slidesToShow: 4,
4569 slidesToScroll: 3,
4570 infinite: false,
4571 prevArrow: '.thumbnails-slider__prev--' + this.settings.sectionId,
4572 nextArrow: '.thumbnails-slider__next--' + this.settings.sectionId,
4573 responsive: [
4574 {
4575 breakpoint: 321,
4576 settings: {
4577 slidesToShow: 3
4578 }
4579 }
4580 ]
4581 };
4582
4583 $(this.selectors.productThumbs).slick(options);
4584
4585 // Accessibility concerns not yet fixed in Slick Slider
4586 $(this.selectors.productThumbsWrapper, this.$container)
4587 .find('.slick-list')
4588 .removeAttr('aria-live');
4589 $(this.selectors.productThumbsWrapper, this.$container)
4590 .find('.slick-disabled')
4591 .removeAttr('aria-disabled');
4592
4593 this.settings.sliderActive = true;
4594 },
4595
4596 _destroyThumbnailSlider: function() {
4597 $(this.selectors.productThumbs).slick('unslick');
4598 this.settings.sliderActive = false;
4599
4600 // Accessibility concerns not yet fixed in Slick Slider
4601 $(this.selectors.productThumbsWrapper, this.$container)
4602 .find('[tabindex="-1"]')
4603 .removeAttr('tabindex');
4604 },
4605
4606 _liveRegionText: function(variant) {
4607 // Dummy content for live region
4608 var liveRegionText =
4609 '[Availability] [Regular] [$$] [Sale] [$]. [UnitPrice] [$$$]';
4610
4611 if (!variant) {
4612 liveRegionText = theme.strings.unavailable;
4613 return liveRegionText;
4614 }
4615
4616 // Update availability
4617 var availability = variant.available ? '' : theme.strings.soldOut + ',';
4618 liveRegionText = liveRegionText.replace('[Availability]', availability);
4619
4620 // Update pricing information
4621 var regularLabel = '';
4622 var regularPrice = theme.Currency.formatMoney(
4623 variant.price,
4624 theme.moneyFormat
4625 );
4626 var saleLabel = '';
4627 var salePrice = '';
4628 var unitLabel = '';
4629 var unitPrice = '';
4630
4631 if (variant.compare_at_price > variant.price) {
4632 regularLabel = theme.strings.regularPrice;
4633 regularPrice =
4634 theme.Currency.formatMoney(
4635 variant.compare_at_price,
4636 theme.moneyFormat
4637 ) + ',';
4638 saleLabel = theme.strings.sale;
4639 salePrice = theme.Currency.formatMoney(
4640 variant.price,
4641 theme.moneyFormat
4642 );
4643 }
4644
4645 if (variant.unit_price) {
4646 unitLabel = theme.strings.unitPrice;
4647 unitPrice =
4648 theme.Currency.formatMoney(variant.unit_price, theme.moneyFormat) +
4649 ' ' +
4650 theme.strings.unitPriceSeparator +
4651 ' ' +
4652 this._getBaseUnit(variant);
4653 }
4654
4655 liveRegionText = liveRegionText
4656 .replace('[Regular]', regularLabel)
4657 .replace('[$$]', regularPrice)
4658 .replace('[Sale]', saleLabel)
4659 .replace('[$]', salePrice)
4660 .replace('[UnitPrice]', unitLabel)
4661 .replace('[$$$]', unitPrice)
4662 .trim();
4663
4664 return liveRegionText;
4665 },
4666
4667 _updateLiveRegion: function(evt) {
4668 var variant = evt.variant;
4669 var liveRegion = this.container.querySelector(
4670 this.selectors.productStatus
4671 );
4672 liveRegion.innerHTML = this._liveRegionText(variant);
4673 liveRegion.setAttribute('aria-hidden', false);
4674
4675 // hide content from accessibility tree after announcement
4676 setTimeout(function() {
4677 liveRegion.setAttribute('aria-hidden', true);
4678 }, 1000);
4679 },
4680
4681 _updateAddToCart: function(evt) {
4682 var variant = evt.variant;
4683
4684 if (variant) {
4685 if (variant.available) {
4686 this.$addToCart
4687 .removeAttr('aria-disabled')
4688 .attr('aria-label', theme.strings.addToCart);
4689 $(this.selectors.addToCartText, this.$container).text(
4690 theme.strings.addToCart
4691 );
4692 this.$shopifyPaymentButton.show();
4693 } else {
4694 // The variant doesn't exist, disable submit button and change the text.
4695 // This may be an error or notice that a specific variant is not available.
4696 this.$addToCart
4697 .attr('aria-disabled', true)
4698 .attr('aria-label', theme.strings.soldOut);
4699 $(this.selectors.addToCartText, this.$container).text(
4700 theme.strings.soldOut
4701 );
4702 this.$shopifyPaymentButton.hide();
4703 }
4704 } else {
4705 this.$addToCart
4706 .attr('aria-disabled', true)
4707 .attr('aria-label', theme.strings.unavailable);
4708 $(this.selectors.addToCartText, this.$container).text(
4709 theme.strings.unavailable
4710 );
4711 this.$shopifyPaymentButton.hide();
4712 }
4713 },
4714
4715 _updateAvailability: function(evt) {
4716 // remove error message if one is showing
4717 this._hideErrorMessage();
4718
4719 // update form submit
4720 this._updateAddToCart(evt);
4721 // update live region
4722 this._updateLiveRegion(evt);
4723
4724 this._updatePrice(evt);
4725 },
4726
4727 _updateImages: function(evt) {
4728 var variant = evt.variant;
4729 var imageId = variant.featured_image.id;
4730
4731 this._switchImage(imageId);
4732 this._setActiveThumbnail(imageId);
4733 },
4734
4735 _updatePrice: function(evt) {
4736 var variant = evt.variant;
4737
4738 var $priceContainer = $(this.selectors.priceContainer, this.$container);
4739 var $regularPrice = $(this.selectors.regularPrice, $priceContainer);
4740 var $salePrice = $(this.selectors.salePrice, $priceContainer);
4741 var $unitPrice = $(this.selectors.unitPrice, $priceContainer);
4742 var $unitPriceBaseUnit = $(
4743 this.selectors.unitPriceBaseUnit,
4744 $priceContainer
4745 );
4746
4747 // Reset product price state
4748 $priceContainer
4749 .removeClass(this.classes.productUnavailable)
4750 .removeClass(this.classes.productOnSale)
4751 .removeClass(this.classes.productUnitAvailable)
4752 .removeAttr('aria-hidden');
4753
4754 // Unavailable
4755 if (!variant) {
4756 $priceContainer
4757 .addClass(this.classes.productUnavailable)
4758 .attr('aria-hidden', true);
4759 return;
4760 }
4761
4762 // On sale
4763 if (variant.compare_at_price > variant.price) {
4764 $regularPrice.html(
4765 theme.Currency.formatMoney(
4766 variant.compare_at_price,
4767 theme.moneyFormat
4768 )
4769 );
4770 $salePrice.html(
4771 theme.Currency.formatMoney(variant.price, theme.moneyFormat)
4772 );
4773 $priceContainer.addClass(this.classes.productOnSale);
4774 } else {
4775 // Regular price
4776 $regularPrice.html(
4777 theme.Currency.formatMoney(variant.price, theme.moneyFormat)
4778 );
4779 }
4780
4781 // Unit price
4782 if (variant.unit_price) {
4783 $unitPrice.html(
4784 theme.Currency.formatMoney(variant.unit_price, theme.moneyFormat)
4785 );
4786 $unitPriceBaseUnit.html(this._getBaseUnit(variant));
4787 $priceContainer.addClass(this.classes.productUnitAvailable);
4788 }
4789 },
4790
4791 _getBaseUnit: function(variant) {
4792 return variant.unit_price_measurement.reference_value === 1
4793 ? variant.unit_price_measurement.reference_unit
4794 : variant.unit_price_measurement.reference_value +
4795 variant.unit_price_measurement.reference_unit;
4796 },
4797
4798 _updateSKU: function(evt) {
4799 var variant = evt.variant;
4800
4801 // Update the sku
4802 $(this.selectors.SKU).html(variant.sku);
4803 },
4804
4805 onUnload: function() {
4806 this.$container.off(this.settings.namespace);
4807 }
4808 });
4809
4810 function _enableZoom(el) {
4811 var zoomUrl = $(el).data('zoom');
4812 $(el).zoom({
4813 url: zoomUrl
4814 });
4815 }
4816
4817 function _destroyZoom(el) {
4818 $(el).trigger('zoom.destroy');
4819 }
4820
4821 return Product;
4822})();
4823
4824theme.ProductRecommendations = (function() {
4825 function ProductRecommendations(container) {
4826 this.$container = $(container);
4827
4828 var productId = this.$container.data('productId');
4829 var recommendationsSectionUrl =
4830 '/recommendations/products?§ion_id=product-recommendations&product_id=' +
4831 productId +
4832 '&limit=4';
4833
4834 $.get(recommendationsSectionUrl).then(
4835 function(section) {
4836 var recommendationsMarkup = $(section).html();
4837 if (recommendationsMarkup.trim() !== '') {
4838 this.$container.html(recommendationsMarkup);
4839 }
4840 }.bind(this)
4841 );
4842 }
4843
4844 return ProductRecommendations;
4845})();
4846
4847theme.Quotes = (function() {
4848 var config = {
4849 mediaQuerySmall: 'screen and (max-width: 749px)',
4850 mediaQueryMediumUp: 'screen and (min-width: 750px)',
4851 slideCount: 0
4852 };
4853 var defaults = {
4854 accessibility: true,
4855 arrows: false,
4856 dots: true,
4857 autoplay: false,
4858 touchThreshold: 20,
4859 slidesToShow: 3,
4860 slidesToScroll: 3
4861 };
4862
4863 function Quotes(container) {
4864 var $container = (this.$container = $(container));
4865 var sectionId = $container.attr('data-section-id');
4866 var wrapper = (this.wrapper = '.quotes-wrapper');
4867 var slider = (this.slider = '#Quotes-' + sectionId);
4868 var $slider = $(slider, wrapper);
4869
4870 var sliderActive = false;
4871 var mobileOptions = $.extend({}, defaults, {
4872 slidesToShow: 1,
4873 slidesToScroll: 1,
4874 adaptiveHeight: true
4875 });
4876
4877 config.slideCount = $slider.data('count');
4878
4879 // Override slidesToShow/Scroll if there are not enough blocks
4880 if (config.slideCount < defaults.slidesToShow) {
4881 defaults.slidesToShow = config.slideCount;
4882 defaults.slidesToScroll = config.slideCount;
4883 }
4884
4885 $slider.on('init', this.a11y.bind(this));
4886
4887 enquire.register(config.mediaQuerySmall, {
4888 match: function() {
4889 initSlider($slider, mobileOptions);
4890 }
4891 });
4892
4893 enquire.register(config.mediaQueryMediumUp, {
4894 match: function() {
4895 initSlider($slider, defaults);
4896 }
4897 });
4898
4899 function initSlider(sliderObj, args) {
4900 if (sliderActive) {
4901 sliderObj.slick('unslick');
4902 sliderActive = false;
4903 }
4904
4905 sliderObj.slick(args);
4906 sliderActive = true;
4907 }
4908 }
4909
4910 Quotes.prototype = _.assignIn({}, Quotes.prototype, {
4911 onUnload: function() {
4912 enquire.unregister(config.mediaQuerySmall);
4913 enquire.unregister(config.mediaQueryMediumUp);
4914
4915 $(this.slider, this.wrapper).slick('unslick');
4916 },
4917
4918 onBlockSelect: function(evt) {
4919 // Ignore the cloned version
4920 var $slide = $(
4921 '.quotes-slide--' + evt.detail.blockId + ':not(.slick-cloned)'
4922 );
4923 var slideIndex = $slide.data('slick-index');
4924
4925 // Go to selected slide, pause autoplay
4926 $(this.slider, this.wrapper).slick('slickGoTo', slideIndex);
4927 },
4928
4929 a11y: function(event, obj) {
4930 var $list = obj.$list;
4931 var $wrapper = $(this.wrapper, this.$container);
4932
4933 // Remove default Slick aria-live attr until slider is focused
4934 $list.removeAttr('aria-live');
4935
4936 // When an element in the slider is focused set aria-live
4937 $wrapper.on('focusin', function(evt) {
4938 if ($wrapper.has(evt.target).length) {
4939 $list.attr('aria-live', 'polite');
4940 }
4941 });
4942
4943 // Remove aria-live
4944 $wrapper.on('focusout', function(evt) {
4945 if ($wrapper.has(evt.target).length) {
4946 $list.removeAttr('aria-live');
4947 }
4948 });
4949 }
4950 });
4951
4952 return Quotes;
4953})();
4954
4955theme.slideshows = {};
4956
4957theme.SlideshowSection = (function() {
4958 function SlideshowSection(container) {
4959 var $container = (this.$container = $(container));
4960 var sectionId = $container.attr('data-section-id');
4961 var slideshow = (this.slideshow = '#Slideshow-' + sectionId);
4962
4963 theme.slideshows[slideshow] = new theme.Slideshow(slideshow, sectionId);
4964 }
4965
4966 return SlideshowSection;
4967})();
4968
4969theme.SlideshowSection.prototype = _.assignIn(
4970 {},
4971 theme.SlideshowSection.prototype,
4972 {
4973 onUnload: function() {
4974 delete theme.slideshows[this.slideshow];
4975 },
4976
4977 onBlockSelect: function(evt) {
4978 var $slideshow = $(this.slideshow);
4979 var adaptHeight = $slideshow.data('adapt-height');
4980
4981 if (adaptHeight) {
4982 theme.slideshows[this.slideshow].setSlideshowHeight();
4983 }
4984
4985 // Ignore the cloned version
4986 var $slide = $(
4987 '.slideshow__slide--' + evt.detail.blockId + ':not(.slick-cloned)'
4988 );
4989 var slideIndex = $slide.data('slick-index');
4990
4991 // Go to selected slide, pause auto-rotate
4992 $slideshow.slick('slickGoTo', slideIndex).slick('slickPause');
4993 },
4994
4995 onBlockDeselect: function() {
4996 // Resume auto-rotate
4997 $(this.slideshow).slick('slickPlay');
4998 }
4999 }
5000);
5001
5002theme.slideshows = {};
5003
5004theme.VideoSection = (function() {
5005 function VideoSection(container) {
5006 var $container = (this.$container = $(container));
5007
5008 $('.video', $container).each(function() {
5009 var $el = $(this);
5010 theme.Video.init($el);
5011 theme.Video.editorLoadVideo($el.attr('id'));
5012 });
5013 }
5014
5015 return VideoSection;
5016})();
5017
5018theme.VideoSection.prototype = _.assignIn({}, theme.VideoSection.prototype, {
5019 onUnload: function() {
5020 theme.Video.removeEvents();
5021 }
5022});
5023
5024theme.heros = {};
5025
5026theme.HeroSection = (function() {
5027 function HeroSection(container) {
5028 var $container = (this.$container = $(container));
5029 var sectionId = $container.attr('data-section-id');
5030 var hero = '#Hero-' + sectionId;
5031 theme.heros[hero] = new theme.Hero(hero, sectionId);
5032 }
5033
5034 return HeroSection;
5035})();
5036
5037
5038$(document).ready(function() {
5039 var sections = new theme.Sections();
5040
5041 sections.register('cart-template', theme.Cart);
5042 sections.register('product', theme.Product);
5043 sections.register('collection-template', theme.Filters);
5044 sections.register('product-template', theme.Product);
5045 sections.register('header-section', theme.HeaderSection);
5046 sections.register('map', theme.Maps);
5047 sections.register('slideshow-section', theme.SlideshowSection);
5048 sections.register('video-section', theme.VideoSection);
5049 sections.register('quotes', theme.Quotes);
5050 sections.register('hero-section', theme.HeroSection);
5051 sections.register('product-recommendations', theme.ProductRecommendations);
5052});
5053
5054theme.init = function() {
5055 theme.customerTemplates.init();
5056
5057 // Theme-specific selectors to make tables scrollable
5058 var tableSelectors = '.rte table,' + '.custom__item-inner--html table';
5059
5060 slate.rte.wrapTable({
5061 $tables: $(tableSelectors),
5062 tableWrapperClass: 'scrollable-wrapper'
5063 });
5064
5065 // Theme-specific selectors to make iframes responsive
5066 var iframeSelectors =
5067 '.rte iframe[src*="youtube.com/embed"],' +
5068 '.rte iframe[src*="player.vimeo"],' +
5069 '.custom__item-inner--html iframe[src*="youtube.com/embed"],' +
5070 '.custom__item-inner--html iframe[src*="player.vimeo"]';
5071
5072 slate.rte.wrapIframe({
5073 $iframes: $(iframeSelectors),
5074 iframeWrapperClass: 'video-wrapper'
5075 });
5076
5077 // Common a11y fixes
5078 slate.a11y.pageLinkFocus($(window.location.hash));
5079
5080 $('.in-page-link').on('click', function(evt) {
5081 slate.a11y.pageLinkFocus($(evt.currentTarget.hash));
5082 });
5083
5084 $('a[href="#"]').on('click', function(evt) {
5085 evt.preventDefault();
5086 });
5087
5088 slate.a11y.accessibleLinks({
5089 messages: {
5090 newWindow: theme.strings.newWindow,
5091 external: theme.strings.external,
5092 newWindowExternal: theme.strings.newWindowExternal
5093 },
5094 $links: $('a[href]:not([aria-describedby], .product-single__thumbnail)')
5095 });
5096
5097 theme.FormStatus.init();
5098
5099 var selectors = {
5100 image: '[data-image]',
5101 imagePlaceholder: '[data-image-placeholder]',
5102 imageWithPlaceholderWrapper: '[data-image-with-placeholder-wrapper]'
5103 };
5104
5105 var classes = {
5106 hidden: 'hide'
5107 };
5108
5109 $(document).on('lazyloaded', function(e) {
5110 var $target = $(e.target);
5111
5112 if (!$target.is(selectors.image)) {
5113 return;
5114 }
5115
5116 $target
5117 .closest(selectors.imageWithPlaceholderWrapper)
5118 .find(selectors.imagePlaceholder)
5119 .addClass(classes.hidden);
5120 });
5121
5122 // When the theme loads, lazysizes might load images before the "lazyloaded"
5123 // event listener has been attached. When this happens, the following function
5124 // hides the loading placeholders.
5125 function onLoadHideLazysizesAnimation() {
5126 $(selectors.image + '.lazyloaded')
5127 .closest(selectors.imageWithPlaceholderWrapper)
5128 .find(selectors.imagePlaceholder)
5129 .addClass(classes.hidden);
5130 }
5131
5132 onLoadHideLazysizesAnimation();
5133};
5134
5135$(theme.init);