· 5 years ago · Jan 20, 2021, 02:38 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.addEventListener(
11 'shopify:section:load',
12 this._onSectionLoad.bind(this)
13 );
14 document.addEventListener(
15 'shopify:section:unload',
16 this._onSectionUnload.bind(this)
17 );
18 document.addEventListener(
19 'shopify:section:select',
20 this._onSelect.bind(this)
21 );
22 document.addEventListener(
23 'shopify:section:deselect',
24 this._onDeselect.bind(this)
25 );
26 document.addEventListener(
27 'shopify:block:select',
28 this._onBlockSelect.bind(this)
29 );
30 document.addEventListener(
31 'shopify:block:deselect',
32 this._onBlockDeselect.bind(this)
33 );
34};
35
36theme.Sections.prototype = Object.assign({}, theme.Sections.prototype, {
37 _createInstance: function(container, constructor) {
38 var id = container.getAttribute('data-section-id');
39 var type = container.getAttribute('data-section-type');
40
41 constructor = constructor || this.constructors[type];
42
43 if (typeof constructor === 'undefined') {
44 return;
45 }
46
47 var instance = Object.assign(new constructor(container), {
48 id: id,
49 type: type,
50 container: container
51 });
52
53 this.instances.push(instance);
54 },
55
56 _onSectionLoad: function(evt) {
57 var container = document.querySelector(
58 '[data-section-id="' + evt.detail.sectionId + '"]'
59 );
60
61 if (container) {
62 this._createInstance(container);
63 }
64 },
65
66 _onSectionUnload: function(evt) {
67 this.instances = this.instances.filter(function(instance) {
68 var isEventInstance = instance.id === evt.detail.sectionId;
69
70 if (isEventInstance) {
71 if (typeof instance.onUnload === 'function') {
72 instance.onUnload(evt);
73 }
74 }
75
76 return !isEventInstance;
77 });
78 },
79
80 _onSelect: function(evt) {
81 // eslint-disable-next-line no-shadow
82 var instance = this.instances.find(function(instance) {
83 return instance.id === evt.detail.sectionId;
84 });
85
86 if (
87 typeof instance !== 'undefined' &&
88 typeof instance.onSelect === 'function'
89 ) {
90 instance.onSelect(evt);
91 }
92 },
93
94 _onDeselect: function(evt) {
95 // eslint-disable-next-line no-shadow
96 var instance = this.instances.find(function(instance) {
97 return instance.id === evt.detail.sectionId;
98 });
99
100 if (
101 typeof instance !== 'undefined' &&
102 typeof instance.onDeselect === 'function'
103 ) {
104 instance.onDeselect(evt);
105 }
106 },
107
108 _onBlockSelect: function(evt) {
109 // eslint-disable-next-line no-shadow
110 var instance = this.instances.find(function(instance) {
111 return instance.id === evt.detail.sectionId;
112 });
113
114 if (
115 typeof instance !== 'undefined' &&
116 typeof instance.onBlockSelect === 'function'
117 ) {
118 instance.onBlockSelect(evt);
119 }
120 },
121
122 _onBlockDeselect: function(evt) {
123 // eslint-disable-next-line no-shadow
124 var instance = this.instances.find(function(instance) {
125 return instance.id === evt.detail.sectionId;
126 });
127
128 if (
129 typeof instance !== 'undefined' &&
130 typeof instance.onBlockDeselect === 'function'
131 ) {
132 instance.onBlockDeselect(evt);
133 }
134 },
135
136 register: function(type, constructor) {
137 this.constructors[type] = constructor;
138
139 document.querySelectorAll('[data-section-type="' + type + '"]').forEach(
140 function(container) {
141 this._createInstance(container, constructor);
142 }.bind(this)
143 );
144 }
145});
146
147window.slate = window.slate || {};
148
149/**
150 * Slate utilities
151 * -----------------------------------------------------------------------------
152 * A collection of useful utilities to help build your theme
153 *
154 *
155 * @namespace utils
156 */
157
158slate.utils = {
159 /**
160 * Get the query params in a Url
161 * Ex
162 * https://mysite.com/search?q=noodles&b
163 * getParameterByName('q') = "noodles"
164 * getParameterByName('b') = "" (empty value)
165 * getParameterByName('test') = null (absent)
166 */
167 getParameterByName: function(name, url) {
168 if (!url) url = window.location.href;
169 name = name.replace(/[[\]]/g, '\\$&');
170 var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
171 results = regex.exec(url);
172 if (!results) return null;
173 if (!results[2]) return '';
174 return decodeURIComponent(results[2].replace(/\+/g, ' '));
175 },
176
177 resizeSelects: function(selects) {
178 selects.forEach(function(select) {
179 var arrowWidth = 55;
180
181 var test = document.createElement('span');
182 test.innerHTML = select.selectedOptions[0].label;
183
184 document.querySelector('.site-footer').appendChild(test);
185
186 var width = test.offsetWidth + arrowWidth;
187 test.remove();
188
189 select.style.width = width + 'px';
190 });
191 },
192
193 keyboardKeys: {
194 TAB: 9,
195 ENTER: 13,
196 ESCAPE: 27,
197 LEFTARROW: 37,
198 RIGHTARROW: 39
199 }
200};
201
202window.slate = window.slate || {};
203
204/**
205 * iFrames
206 * -----------------------------------------------------------------------------
207 * Wrap videos in div to force responsive layout.
208 *
209 * @namespace iframes
210 */
211
212slate.rte = {
213 /**
214 * Wrap tables in a container div to make them scrollable when needed
215 *
216 * @param {object} options - Options to be used
217 * @param {NodeList} options.tables - Elements of the table(s) to wrap
218 * @param {string} options.tableWrapperClass - table wrapper class name
219 */
220 wrapTable: function(options) {
221 options.tables.forEach(function(table) {
222 var wrapper = document.createElement('div');
223 wrapper.classList.add(options.tableWrapperClass);
224
225 table.parentNode.insertBefore(wrapper, table);
226 wrapper.appendChild(table);
227 });
228 },
229
230 /**
231 * Wrap iframes in a container div to make them responsive
232 *
233 * @param {object} options - Options to be used
234 * @param {NodeList} options.iframes - Elements of the iframe(s) to wrap
235 * @param {string} options.iframeWrapperClass - class name used on the wrapping div
236 */
237 wrapIframe: function(options) {
238 options.iframes.forEach(function(iframe) {
239 var wrapper = document.createElement('div');
240 wrapper.classList.add(options.iframeWrapperClass);
241
242 iframe.parentNode.insertBefore(wrapper, iframe);
243 wrapper.appendChild(iframe);
244
245 iframe.src = iframe.src;
246 });
247 }
248};
249
250window.slate = window.slate || {};
251
252/**
253 * A11y Helpers
254 * -----------------------------------------------------------------------------
255 * A collection of useful functions that help make your theme more accessible
256 * to users with visual impairments.
257 *
258 *
259 * @namespace a11y
260 */
261
262slate.a11y = {
263 state: {
264 firstFocusable: null,
265 lastFocusable: null
266 },
267 /**
268 * For use when focus shifts to a container rather than a link
269 * eg for In-page links, after scroll, focus shifts to content area so that
270 * next `tab` is where user expects
271 *
272 * @param {HTMLElement} element - The element to be acted upon
273 */
274 pageLinkFocus: function(element) {
275 if (!element) return;
276 var focusClass = 'js-focus-hidden';
277
278 element.setAttribute('tabIndex', '-1');
279 element.focus();
280 element.classList.add(focusClass);
281 element.addEventListener('blur', callback, { once: true });
282
283 function callback() {
284 element.classList.remove(focusClass);
285 element.removeAttribute('tabindex');
286 }
287 },
288
289 /**
290 * Traps the focus in a particular container
291 *
292 * @param {object} options - Options to be used
293 * @param {HTMLElement} options.container - Container to trap focus within
294 * @param {HTMLElement} options.elementToFocus - Element to be focused when focus leaves container
295 */
296 trapFocus: function(options) {
297 var focusableElements = Array.from(
298 options.container.querySelectorAll(
299 'button, [href], input, select, textarea, [tabindex]:not([tabindex^="-"])'
300 )
301 ).filter(function(element) {
302 var width = element.offsetWidth;
303 var height = element.offsetHeight;
304
305 return (
306 width !== 0 &&
307 height !== 0 &&
308 getComputedStyle(element).getPropertyValue('display') !== 'none'
309 );
310 });
311
312 this.state.firstFocusable = focusableElements[0];
313 this.state.lastFocusable = focusableElements[focusableElements.length - 1];
314
315 if (!options.elementToFocus) {
316 options.elementToFocus = options.container;
317 }
318
319 options.container.setAttribute('tabindex', '-1');
320 options.elementToFocus.focus();
321
322 this._setupHandlers();
323
324 document.addEventListener('focusin', this._onFocusInHandler);
325 document.addEventListener('focusout', this._onFocusOutHandler);
326 },
327
328 _setupHandlers: function() {
329 if (!this._onFocusInHandler) {
330 this._onFocusInHandler = this._onFocusIn.bind(this);
331 }
332
333 if (!this._onFocusOutHandler) {
334 this._onFocusOutHandler = this._onFocusIn.bind(this);
335 }
336
337 if (!this._manageFocusHandler) {
338 this._manageFocusHandler = this._manageFocus.bind(this);
339 }
340 },
341
342 _onFocusOut: function() {
343 document.removeEventListener('keydown', this._manageFocusHandler);
344 },
345
346 _onFocusIn: function(evt) {
347 if (
348 evt.target !== this.state.lastFocusable &&
349 evt.target !== this.state.firstFocusable
350 )
351 return;
352
353 document.addEventListener('keydown', this._manageFocusHandler);
354 },
355
356 _manageFocus: function(evt) {
357 if (evt.keyCode !== slate.utils.keyboardKeys.TAB) return;
358
359 /**
360 * On the last focusable element and tab forward,
361 * focus the first element.
362 */
363 if (evt.target === this.state.lastFocusable && !evt.shiftKey) {
364 evt.preventDefault();
365 this.state.firstFocusable.focus();
366 }
367 /**
368 * On the first focusable element and tab backward,
369 * focus the last element.
370 */
371 if (evt.target === this.state.firstFocusable && evt.shiftKey) {
372 evt.preventDefault();
373 this.state.lastFocusable.focus();
374 }
375 },
376
377 /**
378 * Removes the trap of focus in a particular container
379 *
380 * @param {object} options - Options to be used
381 * @param {HTMLElement} options.container - Container to trap focus within
382 */
383 removeTrapFocus: function(options) {
384 if (options.container) {
385 options.container.removeAttribute('tabindex');
386 }
387 document.removeEventListener('focusin', this._onFocusInHandler);
388 },
389
390 /**
391 * Add aria-describedby attribute to external and new window links
392 *
393 * @param {object} options - Options to be used
394 * @param {object} options.messages - Custom messages to be used
395 * @param {HTMLElement} options.links - Specific links to be targeted
396 */
397 accessibleLinks: function(options) {
398 var body = document.querySelector('body');
399
400 var idSelectors = {
401 newWindow: 'a11y-new-window-message',
402 external: 'a11y-external-message',
403 newWindowExternal: 'a11y-new-window-external-message'
404 };
405
406 if (options.links === undefined || !options.links.length) {
407 options.links = document.querySelectorAll(
408 'a[href]:not([aria-describedby])'
409 );
410 }
411
412 function generateHTML(customMessages) {
413 if (typeof customMessages !== 'object') {
414 customMessages = {};
415 }
416
417 var messages = Object.assign(
418 {
419 newWindow: 'Opens in a new window.',
420 external: 'Opens external website.',
421 newWindowExternal: 'Opens external website in a new window.'
422 },
423 customMessages
424 );
425
426 var container = document.createElement('ul');
427 var htmlMessages = '';
428
429 for (var message in messages) {
430 htmlMessages +=
431 '<li id=' + idSelectors[message] + '>' + messages[message] + '</li>';
432 }
433
434 container.setAttribute('hidden', true);
435 container.innerHTML = htmlMessages;
436
437 body.appendChild(container);
438 }
439
440 function _externalSite(link) {
441 var hostname = window.location.hostname;
442
443 return link.hostname !== hostname;
444 }
445
446 options.links.forEach(function(link) {
447 var target = link.getAttribute('target');
448 var rel = link.getAttribute('rel');
449 var isExternal = _externalSite(link);
450 var isTargetBlank = target === '_blank';
451
452 if (isExternal) {
453 link.setAttribute('aria-describedby', idSelectors.external);
454 }
455
456 if (isTargetBlank) {
457 if (!rel || rel.indexOf('noopener') === -1) {
458 var relValue = rel === undefined ? '' : rel + ' ';
459 relValue = relValue + 'noopener';
460 link.setAttribute('rel', relValue);
461 }
462
463 link.setAttribute('aria-describedby', idSelectors.newWindow);
464 }
465
466 if (isExternal && isTargetBlank) {
467 link.setAttribute('aria-describedby', idSelectors.newWindowExternal);
468 }
469 });
470
471 generateHTML(options.messages);
472 }
473};
474
475/**
476 * Image Helper Functions
477 * -----------------------------------------------------------------------------
478 * A collection of functions that help with basic image operations.
479 *
480 */
481
482theme.Images = (function() {
483 /**
484 * Preloads an image in memory and uses the browsers cache to store it until needed.
485 *
486 * @param {Array} images - A list of image urls
487 * @param {String} size - A shopify image size attribute
488 */
489
490 function preload(images, size) {
491 if (typeof images === 'string') {
492 images = [images];
493 }
494
495 for (var i = 0; i < images.length; i++) {
496 var image = images[i];
497 this.loadImage(this.getSizedImageUrl(image, size));
498 }
499 }
500
501 /**
502 * Loads and caches an image in the browsers cache.
503 * @param {string} path - An image url
504 */
505 function loadImage(path) {
506 new Image().src = path;
507 }
508
509 /**
510 * Swaps the src of an image for another OR returns the imageURL to the callback function
511 * @param image
512 * @param element
513 * @param callback
514 */
515 function switchImage(image, element, callback) {
516 var size = this.imageSize(element.src);
517 var imageUrl = this.getSizedImageUrl(image.src, size);
518
519 if (callback) {
520 callback(imageUrl, image, element); // eslint-disable-line callback-return
521 } else {
522 element.src = imageUrl;
523 }
524 }
525
526 /**
527 * +++ Useful
528 * Find the Shopify image attribute size
529 *
530 * @param {string} src
531 * @returns {null}
532 */
533 function imageSize(src) {
534 var match = src.match(
535 /.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\\.@]/
536 );
537
538 if (match !== null) {
539 if (match[2] !== undefined) {
540 return match[1] + match[2];
541 } else {
542 return match[1];
543 }
544 } else {
545 return null;
546 }
547 }
548
549 /**
550 * +++ Useful
551 * Adds a Shopify size attribute to a URL
552 *
553 * @param src
554 * @param size
555 * @returns {*}
556 */
557 function getSizedImageUrl(src, size) {
558 if (size === null) {
559 return src;
560 }
561
562 if (size === 'master') {
563 return this.removeProtocol(src);
564 }
565
566 var match = src.match(
567 /\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i
568 );
569
570 if (match !== null) {
571 var prefix = src.split(match[0]);
572 var suffix = match[0];
573
574 return this.removeProtocol(prefix[0] + '_' + size + suffix);
575 }
576
577 return null;
578 }
579
580 function removeProtocol(path) {
581 return path.replace(/http(s)?:/, '');
582 }
583
584 return {
585 preload: preload,
586 loadImage: loadImage,
587 switchImage: switchImage,
588 imageSize: imageSize,
589 getSizedImageUrl: getSizedImageUrl,
590 removeProtocol: removeProtocol
591 };
592})();
593
594/**
595 * Currency Helpers
596 * -----------------------------------------------------------------------------
597 * A collection of useful functions that help with currency formatting
598 *
599 * Current contents
600 * - formatMoney - Takes an amount in cents and returns it as a formatted dollar value.
601 *
602 * Alternatives
603 * - Accounting.js - http://openexchangerates.github.io/accounting.js/
604 *
605 */
606
607theme.Currency = (function() {
608 var moneyFormat = '${{amount}}'; // eslint-disable-line camelcase
609
610 function formatMoney(cents, format) {
611 if (typeof cents === 'string') {
612 cents = cents.replace('.', '');
613 }
614 var value = '';
615 var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/;
616 var formatString = format || moneyFormat;
617
618 function formatWithDelimiters(number, precision, thousands, decimal) {
619 thousands = thousands || ',';
620 decimal = decimal || '.';
621
622 if (isNaN(number) || number === null) {
623 return 0;
624 }
625
626 number = (number / 100.0).toFixed(precision);
627
628 var parts = number.split('.');
629 var dollarsAmount = parts[0].replace(
630 /(\d)(?=(\d\d\d)+(?!\d))/g,
631 '$1' + thousands
632 );
633 var centsAmount = parts[1] ? decimal + parts[1] : '';
634
635 return dollarsAmount + centsAmount;
636 }
637
638 switch (formatString.match(placeholderRegex)[1]) {
639 case 'amount':
640 value = formatWithDelimiters(cents, 2);
641 break;
642 case 'amount_no_decimals':
643 value = formatWithDelimiters(cents, 0);
644 break;
645 case 'amount_with_comma_separator':
646 value = formatWithDelimiters(cents, 2, '.', ',');
647 break;
648 case 'amount_no_decimals_with_comma_separator':
649 value = formatWithDelimiters(cents, 0, '.', ',');
650 break;
651 case 'amount_no_decimals_with_space_separator':
652 value = formatWithDelimiters(cents, 0, ' ');
653 break;
654 case 'amount_with_apostrophe_separator':
655 value = formatWithDelimiters(cents, 2, "'");
656 break;
657 }
658
659 return formatString.replace(placeholderRegex, value);
660 }
661
662 return {
663 formatMoney: formatMoney
664 };
665})();
666
667/**
668 * Variant Selection scripts
669 * ------------------------------------------------------------------------------
670 *
671 * Handles change events from the variant inputs in any `cart/add` forms that may
672 * exist. Also updates the master select and triggers updates when the variants
673 * price or image changes.
674 *
675 * @namespace variants
676 */
677
678slate.Variants = (function() {
679 /**
680 * Variant constructor
681 *
682 * @param {object} options - Settings from `product.js`
683 */
684 function Variants(options) {
685 this.container = options.container;
686 this.product = options.product;
687 this.originalSelectorId = options.originalSelectorId;
688 this.enableHistoryState = options.enableHistoryState;
689 this.singleOptions = this.container.querySelectorAll(
690 options.singleOptionSelector
691 );
692 this.currentVariant = this._getVariantFromOptions();
693
694 this.singleOptions.forEach(
695 function(option) {
696 option.addEventListener('change', this._onSelectChange.bind(this));
697 }.bind(this)
698 );
699 }
700
701 Variants.prototype = Object.assign({}, Variants.prototype, {
702 /**
703 * Get the currently selected options from add-to-cart form. Works with all
704 * form input elements.
705 *
706 * @return {array} options - Values of currently selected variants
707 */
708 _getCurrentOptions: function() {
709 var result = [];
710
711 this.singleOptions.forEach(function(option) {
712 var type = option.getAttribute('type');
713 var isRadioOrCheckbox = type === 'radio' || type === 'checkbox';
714
715 if (!isRadioOrCheckbox || option.checked) {
716 result.push({
717 value: option.value,
718 index: option.getAttribute('data-index')
719 });
720 }
721 });
722
723 return result;
724 },
725
726 /**
727 * Find variant based on selected values.
728 *
729 * @param {array} selectedValues - Values of variant inputs
730 * @return {object || undefined} found - Variant object from product.variants
731 */
732 _getVariantFromOptions: function() {
733 var selectedValues = this._getCurrentOptions();
734 var variants = this.product.variants;
735
736 var found = variants.find(function(variant) {
737 return selectedValues.every(function(values) {
738 return variant[values.index] === values.value;
739 });
740 });
741
742 return found;
743 },
744
745 /**
746 * Event handler for when a variant input changes.
747 */
748 _onSelectChange: function() {
749 var variant = this._getVariantFromOptions();
750
751 this.container.dispatchEvent(
752 new CustomEvent('variantChange', {
753 detail: {
754 variant: variant
755 },
756 bubbles: true,
757 cancelable: true
758 })
759 );
760
761 if (!variant) {
762 return;
763 }
764
765 this._updateMasterSelect(variant);
766 this._updateImages(variant);
767 this._updatePrice(variant);
768 this._updateSKU(variant);
769 this.currentVariant = variant;
770
771 if (this.enableHistoryState) {
772 this._updateHistoryState(variant);
773 }
774 },
775
776 /**
777 * Trigger event when variant image changes
778 *
779 * @param {object} variant - Currently selected variant
780 * @return {event} variantImageChange
781 */
782 _updateImages: function(variant) {
783 var variantImage = variant.featured_image || {};
784 var currentVariantImage = this.currentVariant.featured_image || {};
785
786 if (
787 !variant.featured_image ||
788 variantImage.src === currentVariantImage.src
789 ) {
790 return;
791 }
792
793 this.container.dispatchEvent(
794 new CustomEvent('variantImageChange', {
795 detail: {
796 variant: variant
797 },
798 bubbles: true,
799 cancelable: true
800 })
801 );
802 },
803
804 /**
805 * Trigger event when variant price changes.
806 *
807 * @param {object} variant - Currently selected variant
808 * @return {event} variantPriceChange
809 */
810 _updatePrice: function(variant) {
811 if (
812 variant.price === this.currentVariant.price &&
813 variant.compare_at_price === this.currentVariant.compare_at_price
814 ) {
815 return;
816 }
817
818 this.container.dispatchEvent(
819 new CustomEvent('variantPriceChange', {
820 detail: {
821 variant: variant
822 },
823 bubbles: true,
824 cancelable: true
825 })
826 );
827 },
828
829 /**
830 * Trigger event when variant sku changes.
831 *
832 * @param {object} variant - Currently selected variant
833 * @return {event} variantSKUChange
834 */
835 _updateSKU: function(variant) {
836 if (variant.sku === this.currentVariant.sku) {
837 return;
838 }
839
840 this.container.dispatchEvent(
841 new CustomEvent('variantSKUChange', {
842 detail: {
843 variant: variant
844 },
845 bubbles: true,
846 cancelable: true
847 })
848 );
849 },
850
851 /**
852 * Update history state for product deeplinking
853 *
854 * @param {variant} variant - Currently selected variant
855 * @return {k} [description]
856 */
857 _updateHistoryState: function(variant) {
858 if (!history.replaceState || !variant) {
859 return;
860 }
861
862 var newurl =
863 window.location.protocol +
864 '//' +
865 window.location.host +
866 window.location.pathname +
867 '?variant=' +
868 variant.id;
869 window.history.replaceState({ path: newurl }, '', newurl);
870 },
871
872 /**
873 * Update hidden master select of variant change
874 *
875 * @param {variant} variant - Currently selected variant
876 */
877 _updateMasterSelect: function(variant) {
878 var masterSelect = this.container.querySelector(this.originalSelectorId);
879
880 if (!masterSelect) return;
881 masterSelect.value = variant.id;
882 }
883 });
884
885 return Variants;
886})();
887
888this.Shopify = this.Shopify || {};
889this.Shopify.theme = this.Shopify.theme || {};
890this.Shopify.theme.PredictiveSearch = (function() {
891 'use strict';
892
893 function validateQuery(query) {
894 var error;
895
896 if (query === null || query === undefined) {
897 error = new TypeError("'query' is missing");
898 error.type = 'argument';
899 throw error;
900 }
901
902 if (typeof query !== 'string') {
903 error = new TypeError("'query' is not a string");
904 error.type = 'argument';
905 throw error;
906 }
907 }
908
909 function GenericError() {
910 var error = Error.call(this);
911
912 error.name = 'Server error';
913 error.message = 'Something went wrong on the server';
914 error.status = 500;
915
916 return error;
917 }
918
919 function NotFoundError(status) {
920 var error = Error.call(this);
921
922 error.name = 'Not found';
923 error.message = 'Not found';
924 error.status = status;
925
926 return error;
927 }
928
929 function ServerError() {
930 var error = Error.call(this);
931
932 error.name = 'Server error';
933 error.message = 'Something went wrong on the server';
934 error.status = 500;
935
936 return error;
937 }
938
939 function ContentTypeError(status) {
940 var error = Error.call(this);
941
942 error.name = 'Content-Type error';
943 error.message = 'Content-Type was not provided or is of wrong type';
944 error.status = status;
945
946 return error;
947 }
948
949 function JsonParseError(status) {
950 var error = Error.call(this);
951
952 error.name = 'JSON parse error';
953 error.message = 'JSON syntax error';
954 error.status = status;
955
956 return error;
957 }
958
959 function ThrottledError(status, name, message, retryAfter) {
960 var error = Error.call(this);
961
962 error.name = name;
963 error.message = message;
964 error.status = status;
965 error.retryAfter = retryAfter;
966
967 return error;
968 }
969
970 function InvalidParameterError(status, name, message) {
971 var error = Error.call(this);
972
973 error.name = name;
974 error.message = message;
975 error.status = status;
976
977 return error;
978 }
979
980 function ExpectationFailedError(status, name, message) {
981 var error = Error.call(this);
982
983 error.name = name;
984 error.message = message;
985 error.status = status;
986
987 return error;
988 }
989
990 function request(searchUrl, queryParams, query, onSuccess, onError) {
991 var xhr = new XMLHttpRequest();
992 var route = searchUrl + '/suggest.json';
993
994 xhr.onreadystatechange = function() {
995 if (xhr.readyState !== XMLHttpRequest.DONE) {
996 return;
997 }
998
999 var contentType = xhr.getResponseHeader('Content-Type');
1000
1001 if (xhr.status >= 500) {
1002 onError(new ServerError());
1003
1004 return;
1005 }
1006
1007 if (xhr.status === 404) {
1008 onError(new NotFoundError(xhr.status));
1009
1010 return;
1011 }
1012
1013 if (
1014 typeof contentType !== 'string' ||
1015 contentType.toLowerCase().match('application/json') === null
1016 ) {
1017 onError(new ContentTypeError(xhr.status));
1018
1019 return;
1020 }
1021
1022 if (xhr.status === 417) {
1023 try {
1024 var invalidParameterJson = JSON.parse(xhr.responseText);
1025
1026 onError(
1027 new InvalidParameterError(
1028 xhr.status,
1029 invalidParameterJson.message,
1030 invalidParameterJson.description
1031 )
1032 );
1033 } catch (error) {
1034 onError(new JsonParseError(xhr.status));
1035 }
1036
1037 return;
1038 }
1039
1040 if (xhr.status === 422) {
1041 try {
1042 var expectationFailedJson = JSON.parse(xhr.responseText);
1043
1044 onError(
1045 new ExpectationFailedError(
1046 xhr.status,
1047 expectationFailedJson.message,
1048 expectationFailedJson.description
1049 )
1050 );
1051 } catch (error) {
1052 onError(new JsonParseError(xhr.status));
1053 }
1054
1055 return;
1056 }
1057
1058 if (xhr.status === 429) {
1059 try {
1060 var throttledJson = JSON.parse(xhr.responseText);
1061
1062 onError(
1063 new ThrottledError(
1064 xhr.status,
1065 throttledJson.message,
1066 throttledJson.description,
1067 xhr.getResponseHeader('Retry-After')
1068 )
1069 );
1070 } catch (error) {
1071 onError(new JsonParseError(xhr.status));
1072 }
1073
1074 return;
1075 }
1076
1077 if (xhr.status === 200) {
1078 try {
1079 var res = JSON.parse(xhr.responseText);
1080 res.query = query;
1081 onSuccess(res);
1082 } catch (error) {
1083 onError(new JsonParseError(xhr.status));
1084 }
1085
1086 return;
1087 }
1088
1089 try {
1090 var genericErrorJson = JSON.parse(xhr.responseText);
1091 onError(
1092 new GenericError(
1093 xhr.status,
1094 genericErrorJson.message,
1095 genericErrorJson.description
1096 )
1097 );
1098 } catch (error) {
1099 onError(new JsonParseError(xhr.status));
1100 }
1101
1102 return;
1103 };
1104
1105 xhr.open(
1106 'get',
1107 route + '?q=' + encodeURIComponent(query) + '&' + queryParams
1108 );
1109
1110 xhr.setRequestHeader('Content-Type', 'application/json');
1111
1112 xhr.send();
1113 }
1114
1115 function Cache(config) {
1116 this._store = {};
1117 this._keys = [];
1118 if (config && config.bucketSize) {
1119 this.bucketSize = config.bucketSize;
1120 } else {
1121 this.bucketSize = 20;
1122 }
1123 }
1124
1125 Cache.prototype.set = function(key, value) {
1126 if (this.count() >= this.bucketSize) {
1127 var deleteKey = this._keys.splice(0, 1);
1128 this.delete(deleteKey);
1129 }
1130
1131 this._keys.push(key);
1132 this._store[key] = value;
1133
1134 return this._store;
1135 };
1136
1137 Cache.prototype.get = function(key) {
1138 return this._store[key];
1139 };
1140
1141 Cache.prototype.has = function(key) {
1142 return Boolean(this._store[key]);
1143 };
1144
1145 Cache.prototype.count = function() {
1146 return Object.keys(this._store).length;
1147 };
1148
1149 Cache.prototype.delete = function(key) {
1150 var exists = Boolean(this._store[key]);
1151 delete this._store[key];
1152 return exists && !this._store[key];
1153 };
1154
1155 function Dispatcher() {
1156 this.events = {};
1157 }
1158
1159 Dispatcher.prototype.on = function(eventName, callback) {
1160 var event = this.events[eventName];
1161 if (!event) {
1162 event = new DispatcherEvent(eventName);
1163 this.events[eventName] = event;
1164 }
1165 event.registerCallback(callback);
1166 };
1167
1168 Dispatcher.prototype.off = function(eventName, callback) {
1169 var event = this.events[eventName];
1170 if (event && event.callbacks.indexOf(callback) > -1) {
1171 event.unregisterCallback(callback);
1172 if (event.callbacks.length === 0) {
1173 delete this.events[eventName];
1174 }
1175 }
1176 };
1177
1178 Dispatcher.prototype.dispatch = function(eventName, payload) {
1179 var event = this.events[eventName];
1180 if (event) {
1181 event.fire(payload);
1182 }
1183 };
1184
1185 function DispatcherEvent(eventName) {
1186 this.eventName = eventName;
1187 this.callbacks = [];
1188 }
1189
1190 DispatcherEvent.prototype.registerCallback = function(callback) {
1191 this.callbacks.push(callback);
1192 };
1193
1194 DispatcherEvent.prototype.unregisterCallback = function(callback) {
1195 var index = this.callbacks.indexOf(callback);
1196 if (index > -1) {
1197 this.callbacks.splice(index, 1);
1198 }
1199 };
1200
1201 DispatcherEvent.prototype.fire = function(payload) {
1202 var callbacks = this.callbacks.slice(0);
1203 callbacks.forEach(function(callback) {
1204 callback(payload);
1205 });
1206 };
1207
1208 function debounce(func, wait) {
1209 var timeout = null;
1210 return function() {
1211 var context = this;
1212 var args = arguments;
1213 clearTimeout(timeout);
1214 timeout = setTimeout(function() {
1215 timeout = null;
1216 func.apply(context, args);
1217 }, wait || 0);
1218 };
1219 }
1220
1221 function objectToQueryParams(obj, parentKey) {
1222 var output = '';
1223 parentKey = parentKey || null;
1224
1225 Object.keys(obj).forEach(function(key) {
1226 var outputKey = key + '=';
1227 if (parentKey) {
1228 outputKey = parentKey + '[' + key + ']';
1229 }
1230
1231 switch (trueTypeOf(obj[key])) {
1232 case 'object':
1233 output += objectToQueryParams(obj[key], parentKey ? outputKey : key);
1234 break;
1235 case 'array':
1236 output += outputKey + '=' + obj[key].join(',') + '&';
1237 break;
1238 default:
1239 if (parentKey) {
1240 outputKey += '=';
1241 }
1242 output += outputKey + encodeURIComponent(obj[key]) + '&';
1243 break;
1244 }
1245 });
1246
1247 return output;
1248 }
1249
1250 function trueTypeOf(obj) {
1251 return Object.prototype.toString
1252 .call(obj)
1253 .slice(8, -1)
1254 .toLowerCase();
1255 }
1256
1257 var DEBOUNCE_RATE = 10;
1258 var requestDebounced = debounce(request, DEBOUNCE_RATE);
1259
1260 function PredictiveSearch(params, searchUrl) {
1261 if (!params) {
1262 throw new TypeError('No params object was specified');
1263 }
1264
1265 this.searchUrl = searchUrl;
1266
1267 this._retryAfter = null;
1268 this._currentQuery = null;
1269
1270 this.dispatcher = new Dispatcher();
1271 this.cache = new Cache({ bucketSize: 40 });
1272
1273 this.queryParams = objectToQueryParams(params);
1274 }
1275
1276 PredictiveSearch.TYPES = {
1277 PRODUCT: 'product',
1278 PAGE: 'page',
1279 ARTICLE: 'article'
1280 };
1281
1282 PredictiveSearch.FIELDS = {
1283 AUTHOR: 'author',
1284 BODY: 'body',
1285 PRODUCT_TYPE: 'product_type',
1286 TAG: 'tag',
1287 TITLE: 'title',
1288 VARIANTS_BARCODE: 'variants.barcode',
1289 VARIANTS_SKU: 'variants.sku',
1290 VARIANTS_TITLE: 'variants.title',
1291 VENDOR: 'vendor'
1292 };
1293
1294 PredictiveSearch.UNAVAILABLE_PRODUCTS = {
1295 SHOW: 'show',
1296 HIDE: 'hide',
1297 LAST: 'last'
1298 };
1299
1300 PredictiveSearch.prototype.query = function query(query) {
1301 try {
1302 validateQuery(query);
1303 } catch (error) {
1304 this.dispatcher.dispatch('error', error);
1305 return;
1306 }
1307
1308 if (query === '') {
1309 return this;
1310 }
1311
1312 this._currentQuery = normalizeQuery(query);
1313 var cacheResult = this.cache.get(this._currentQuery);
1314 if (cacheResult) {
1315 this.dispatcher.dispatch('success', cacheResult);
1316 return this;
1317 }
1318
1319 requestDebounced(
1320 this.searchUrl,
1321 this.queryParams,
1322 query,
1323 function(result) {
1324 this.cache.set(normalizeQuery(result.query), result);
1325 if (normalizeQuery(result.query) === this._currentQuery) {
1326 this._retryAfter = null;
1327 this.dispatcher.dispatch('success', result);
1328 }
1329 }.bind(this),
1330 function(error) {
1331 if (error.retryAfter) {
1332 this._retryAfter = error.retryAfter;
1333 }
1334 this.dispatcher.dispatch('error', error);
1335 }.bind(this)
1336 );
1337
1338 return this;
1339 };
1340
1341 PredictiveSearch.prototype.on = function on(eventName, callback) {
1342 this.dispatcher.on(eventName, callback);
1343
1344 return this;
1345 };
1346
1347 PredictiveSearch.prototype.off = function on(eventName, callback) {
1348 this.dispatcher.off(eventName, callback);
1349
1350 return this;
1351 };
1352
1353 function normalizeQuery(query) {
1354 if (typeof query !== 'string') {
1355 return null;
1356 }
1357
1358 return query
1359 .trim()
1360 .replace(' ', '-')
1361 .toLowerCase();
1362 }
1363
1364 return PredictiveSearch;
1365})();
1366
1367this.Shopify = this.Shopify || {};
1368this.Shopify.theme = this.Shopify.theme || {};
1369this.Shopify.theme.PredictiveSearchComponent = (function(PredictiveSearch) {
1370 'use strict';
1371
1372 PredictiveSearch =
1373 PredictiveSearch && PredictiveSearch.hasOwnProperty('default')
1374 ? PredictiveSearch['default']
1375 : PredictiveSearch;
1376
1377 var DEFAULT_PREDICTIVE_SEARCH_API_CONFIG = {
1378 resources: {
1379 type: [PredictiveSearch.TYPES.PRODUCT],
1380 options: {
1381 unavailable_products: PredictiveSearch.UNAVAILABLE_PRODUCTS.LAST,
1382 fields: [
1383 PredictiveSearch.FIELDS.TITLE,
1384 PredictiveSearch.FIELDS.VENDOR,
1385 PredictiveSearch.FIELDS.PRODUCT_TYPE,
1386 PredictiveSearch.FIELDS.VARIANTS_TITLE
1387 ]
1388 }
1389 }
1390 };
1391
1392 function PredictiveSearchComponent(config) {
1393 // validate config
1394 if (
1395 !config ||
1396 !config.selectors ||
1397 !config.selectors.input ||
1398 !isString(config.selectors.input) ||
1399 !config.selectors.result ||
1400 !isString(config.selectors.result) ||
1401 !config.resultTemplateFct ||
1402 !isFunction(config.resultTemplateFct) ||
1403 !config.numberOfResultsTemplateFct ||
1404 !isFunction(config.numberOfResultsTemplateFct) ||
1405 !config.loadingResultsMessageTemplateFct ||
1406 !isFunction(config.loadingResultsMessageTemplateFct)
1407 ) {
1408 var error = new TypeError(
1409 'PredictiveSearchComponent config is not valid'
1410 );
1411 error.type = 'argument';
1412 throw error;
1413 }
1414
1415 // Find nodes
1416 this.nodes = findNodes(config.selectors);
1417
1418 // Validate nodes
1419 if (!isValidNodes(this.nodes)) {
1420 // eslint-disable-next-line no-console
1421 console.warn('Could not find valid nodes');
1422 return;
1423 }
1424
1425 this.searchUrl = config.searchUrl || '/search';
1426
1427 // Store the keyword that was used for the search
1428 this._searchKeyword = '';
1429
1430 // Assign result template
1431 this.resultTemplateFct = config.resultTemplateFct;
1432
1433 // Assign number of results template
1434 this.numberOfResultsTemplateFct = config.numberOfResultsTemplateFct;
1435
1436 // Assign loading state template function
1437 this.loadingResultsMessageTemplateFct =
1438 config.loadingResultsMessageTemplateFct;
1439
1440 // Assign number of search results
1441 this.numberOfResults = config.numberOfResults || 4;
1442
1443 // Set classes
1444 this.classes = {
1445 visibleVariant: config.visibleVariant
1446 ? config.visibleVariant
1447 : 'predictive-search-wrapper--visible',
1448 itemSelected: config.itemSelectedClass
1449 ? config.itemSelectedClass
1450 : 'predictive-search-item--selected',
1451 clearButtonVisible: config.clearButtonVisibleClass
1452 ? config.clearButtonVisibleClass
1453 : 'predictive-search__clear-button--visible'
1454 };
1455
1456 this.selectors = {
1457 searchResult: config.searchResult
1458 ? config.searchResult
1459 : '[data-search-result]'
1460 };
1461
1462 // Assign callbacks
1463 this.callbacks = assignCallbacks(config);
1464
1465 // Add input attributes
1466 addInputAttributes(this.nodes.input);
1467
1468 // Add input event listeners
1469 this._addInputEventListeners();
1470
1471 // Add body listener
1472 this._addBodyEventListener();
1473
1474 // Add accessibility announcer
1475 this._addAccessibilityAnnouncer();
1476
1477 // Display the reset button if the input is not empty
1478 this._toggleClearButtonVisibility();
1479
1480 // Instantiate Predictive Search API
1481 this.predictiveSearch = new PredictiveSearch(
1482 config.PredictiveSearchAPIConfig
1483 ? config.PredictiveSearchAPIConfig
1484 : DEFAULT_PREDICTIVE_SEARCH_API_CONFIG,
1485 this.searchUrl
1486 );
1487
1488 // Add predictive search success event listener
1489 this.predictiveSearch.on(
1490 'success',
1491 this._handlePredictiveSearchSuccess.bind(this)
1492 );
1493
1494 // Add predictive search error event listener
1495 this.predictiveSearch.on(
1496 'error',
1497 this._handlePredictiveSearchError.bind(this)
1498 );
1499 }
1500
1501 /**
1502 * Private methods
1503 */
1504 function findNodes(selectors) {
1505 return {
1506 input: document.querySelector(selectors.input),
1507 reset: document.querySelector(selectors.reset),
1508 result: document.querySelector(selectors.result)
1509 };
1510 }
1511
1512 function isValidNodes(nodes) {
1513 if (
1514 !nodes ||
1515 !nodes.input ||
1516 !nodes.result ||
1517 nodes.input.tagName !== 'INPUT'
1518 ) {
1519 return false;
1520 }
1521
1522 return true;
1523 }
1524
1525 function assignCallbacks(config) {
1526 return {
1527 onBodyMousedown: config.onBodyMousedown,
1528 onBeforeOpen: config.onBeforeOpen,
1529 onOpen: config.onOpen,
1530 onBeforeClose: config.onBeforeClose,
1531 onClose: config.onClose,
1532 onInputFocus: config.onInputFocus,
1533 onInputKeyup: config.onInputKeyup,
1534 onInputBlur: config.onInputBlur,
1535 onInputReset: config.onInputReset,
1536 onBeforeDestroy: config.onBeforeDestroy,
1537 onDestroy: config.onDestroy
1538 };
1539 }
1540
1541 function addInputAttributes(input) {
1542 input.setAttribute('autocorrect', 'off');
1543 input.setAttribute('autocomplete', 'off');
1544 input.setAttribute('autocapitalize', 'off');
1545 input.setAttribute('spellcheck', 'false');
1546 }
1547
1548 function removeInputAttributes(input) {
1549 input.removeAttribute('autocorrect', 'off');
1550 input.removeAttribute('autocomplete', 'off');
1551 input.removeAttribute('autocapitalize', 'off');
1552 input.removeAttribute('spellcheck', 'false');
1553 }
1554
1555 /**
1556 * Public variables
1557 */
1558 PredictiveSearchComponent.prototype.isResultVisible = false;
1559 PredictiveSearchComponent.prototype.results = {};
1560
1561 /**
1562 * "Private" variables
1563 */
1564 PredictiveSearchComponent.prototype._latencyTimer = null;
1565 PredictiveSearchComponent.prototype._resultNodeClicked = false;
1566
1567 /**
1568 * "Private" instance methods
1569 */
1570 PredictiveSearchComponent.prototype._addInputEventListeners = function() {
1571 var input = this.nodes.input;
1572 var reset = this.nodes.reset;
1573
1574 if (!input) {
1575 return;
1576 }
1577
1578 this._handleInputFocus = this._handleInputFocus.bind(this);
1579 this._handleInputBlur = this._handleInputBlur.bind(this);
1580 this._handleInputKeyup = this._handleInputKeyup.bind(this);
1581 this._handleInputKeydown = this._handleInputKeydown.bind(this);
1582
1583 input.addEventListener('focus', this._handleInputFocus);
1584 input.addEventListener('blur', this._handleInputBlur);
1585 input.addEventListener('keyup', this._handleInputKeyup);
1586 input.addEventListener('keydown', this._handleInputKeydown);
1587
1588 if (reset) {
1589 this._handleInputReset = this._handleInputReset.bind(this);
1590 reset.addEventListener('click', this._handleInputReset);
1591 }
1592 };
1593
1594 PredictiveSearchComponent.prototype._removeInputEventListeners = function() {
1595 var input = this.nodes.input;
1596
1597 input.removeEventListener('focus', this._handleInputFocus);
1598 input.removeEventListener('blur', this._handleInputBlur);
1599 input.removeEventListener('keyup', this._handleInputKeyup);
1600 input.removeEventListener('keydown', this._handleInputKeydown);
1601 };
1602
1603 PredictiveSearchComponent.prototype._addBodyEventListener = function() {
1604 this._handleBodyMousedown = this._handleBodyMousedown.bind(this);
1605
1606 document
1607 .querySelector('body')
1608 .addEventListener('mousedown', this._handleBodyMousedown);
1609 };
1610
1611 PredictiveSearchComponent.prototype._removeBodyEventListener = function() {
1612 document
1613 .querySelector('body')
1614 .removeEventListener('mousedown', this._handleBodyMousedown);
1615 };
1616
1617 PredictiveSearchComponent.prototype._removeClearButtonEventListener = function() {
1618 var reset = this.nodes.reset;
1619
1620 if (!reset) {
1621 return;
1622 }
1623
1624 reset.removeEventListener('click', this._handleInputReset);
1625 };
1626
1627 /**
1628 * Event handlers
1629 */
1630 PredictiveSearchComponent.prototype._handleBodyMousedown = function(evt) {
1631 if (this.isResultVisible && this.nodes !== null) {
1632 if (
1633 evt.target.isEqualNode(this.nodes.input) ||
1634 this.nodes.input.contains(evt.target) ||
1635 evt.target.isEqualNode(this.nodes.result) ||
1636 this.nodes.result.contains(evt.target)
1637 ) {
1638 this._resultNodeClicked = true;
1639 } else {
1640 if (isFunction(this.callbacks.onBodyMousedown)) {
1641 var returnedValue = this.callbacks.onBodyMousedown(this.nodes);
1642 if (isBoolean(returnedValue) && returnedValue) {
1643 this.close();
1644 }
1645 } else {
1646 this.close();
1647 }
1648 }
1649 }
1650 };
1651
1652 PredictiveSearchComponent.prototype._handleInputFocus = function(evt) {
1653 if (isFunction(this.callbacks.onInputFocus)) {
1654 var returnedValue = this.callbacks.onInputFocus(this.nodes);
1655 if (isBoolean(returnedValue) && !returnedValue) {
1656 return false;
1657 }
1658 }
1659
1660 if (evt.target.value.length > 0) {
1661 this._search();
1662 }
1663
1664 return true;
1665 };
1666
1667 PredictiveSearchComponent.prototype._handleInputBlur = function() {
1668 // This has to be done async, to wait for the focus to be on the next
1669 // element and avoid closing the results.
1670 // Example: Going from the input to the reset button.
1671 setTimeout(
1672 function() {
1673 if (isFunction(this.callbacks.onInputBlur)) {
1674 var returnedValue = this.callbacks.onInputBlur(this.nodes);
1675 if (isBoolean(returnedValue) && !returnedValue) {
1676 return false;
1677 }
1678 }
1679
1680 if (document.activeElement.isEqualNode(this.nodes.reset)) {
1681 return false;
1682 }
1683
1684 if (this._resultNodeClicked) {
1685 this._resultNodeClicked = false;
1686 return false;
1687 }
1688
1689 this.close();
1690 }.bind(this)
1691 );
1692
1693 return true;
1694 };
1695
1696 PredictiveSearchComponent.prototype._addAccessibilityAnnouncer = function() {
1697 this._accessibilityAnnouncerDiv = window.document.createElement('div');
1698
1699 this._accessibilityAnnouncerDiv.setAttribute(
1700 'style',
1701 'position: absolute !important; overflow: hidden; clip: rect(0 0 0 0); height: 1px; width: 1px; margin: -1px; padding: 0; border: 0;'
1702 );
1703
1704 this._accessibilityAnnouncerDiv.setAttribute('data-search-announcer', '');
1705 this._accessibilityAnnouncerDiv.setAttribute('aria-live', 'polite');
1706 this._accessibilityAnnouncerDiv.setAttribute('aria-atomic', 'true');
1707
1708 this.nodes.result.parentElement.appendChild(
1709 this._accessibilityAnnouncerDiv
1710 );
1711 };
1712
1713 PredictiveSearchComponent.prototype._removeAccessibilityAnnouncer = function() {
1714 this.nodes.result.parentElement.removeChild(
1715 this._accessibilityAnnouncerDiv
1716 );
1717 };
1718
1719 PredictiveSearchComponent.prototype._updateAccessibilityAttributesAfterSelectingElement = function(
1720 previousSelectedElement,
1721 currentSelectedElement
1722 ) {
1723 // Update the active descendant on the search input
1724 this.nodes.input.setAttribute(
1725 'aria-activedescendant',
1726 currentSelectedElement.id
1727 );
1728
1729 // Unmark the previousSelected elemented as selected
1730 if (previousSelectedElement) {
1731 previousSelectedElement.removeAttribute('aria-selected');
1732 }
1733
1734 // Mark the element as selected
1735 currentSelectedElement.setAttribute('aria-selected', true);
1736 };
1737
1738 PredictiveSearchComponent.prototype._clearAriaActiveDescendant = function() {
1739 this.nodes.input.setAttribute('aria-activedescendant', '');
1740 };
1741
1742 PredictiveSearchComponent.prototype._announceNumberOfResultsFound = function(
1743 results
1744 ) {
1745 var currentAnnouncedMessage = this._accessibilityAnnouncerDiv.innerHTML;
1746 var newMessage = this.numberOfResultsTemplateFct(results);
1747
1748 // If the messages are the same, they won't get announced
1749 // add white space so it gets announced
1750 if (currentAnnouncedMessage === newMessage) {
1751 newMessage = newMessage + ' ';
1752 }
1753
1754 this._accessibilityAnnouncerDiv.innerHTML = newMessage;
1755 };
1756
1757 PredictiveSearchComponent.prototype._announceLoadingState = function() {
1758 this._accessibilityAnnouncerDiv.innerHTML = this.loadingResultsMessageTemplateFct();
1759 };
1760
1761 PredictiveSearchComponent.prototype._handleInputKeyup = function(evt) {
1762 var UP_ARROW_KEY_CODE = 38;
1763 var DOWN_ARROW_KEY_CODE = 40;
1764 var RETURN_KEY_CODE = 13;
1765 var ESCAPE_KEY_CODE = 27;
1766
1767 if (isFunction(this.callbacks.onInputKeyup)) {
1768 var returnedValue = this.callbacks.onInputKeyup(this.nodes);
1769 if (isBoolean(returnedValue) && !returnedValue) {
1770 return false;
1771 }
1772 }
1773
1774 this._toggleClearButtonVisibility();
1775
1776 if (this.isResultVisible && this.nodes !== null) {
1777 if (evt.keyCode === UP_ARROW_KEY_CODE) {
1778 this._navigateOption(evt, 'UP');
1779 return true;
1780 }
1781
1782 if (evt.keyCode === DOWN_ARROW_KEY_CODE) {
1783 this._navigateOption(evt, 'DOWN');
1784 return true;
1785 }
1786
1787 if (evt.keyCode === RETURN_KEY_CODE) {
1788 this._selectOption();
1789 return true;
1790 }
1791
1792 if (evt.keyCode === ESCAPE_KEY_CODE) {
1793 this.close();
1794 }
1795 }
1796
1797 if (evt.target.value.length <= 0) {
1798 this.close();
1799 this._setKeyword('');
1800 } else if (evt.target.value.length > 0) {
1801 this._search();
1802 }
1803
1804 return true;
1805 };
1806
1807 PredictiveSearchComponent.prototype._handleInputKeydown = function(evt) {
1808 var RETURN_KEY_CODE = 13;
1809 var UP_ARROW_KEY_CODE = 38;
1810 var DOWN_ARROW_KEY_CODE = 40;
1811
1812 // Prevent the form default submission if there is a selected option
1813 if (evt.keyCode === RETURN_KEY_CODE && this._getSelectedOption() !== null) {
1814 evt.preventDefault();
1815 }
1816
1817 // Prevent the cursor from moving in the input when using the up and down arrow keys
1818 if (
1819 evt.keyCode === UP_ARROW_KEY_CODE ||
1820 evt.keyCode === DOWN_ARROW_KEY_CODE
1821 ) {
1822 evt.preventDefault();
1823 }
1824 };
1825
1826 PredictiveSearchComponent.prototype._handleInputReset = function(evt) {
1827 evt.preventDefault();
1828
1829 if (isFunction(this.callbacks.onInputReset)) {
1830 var returnedValue = this.callbacks.onInputReset(this.nodes);
1831 if (isBoolean(returnedValue) && !returnedValue) {
1832 return false;
1833 }
1834 }
1835
1836 this.nodes.input.value = '';
1837 this.nodes.input.focus();
1838 this._toggleClearButtonVisibility();
1839 this.close();
1840
1841 return true;
1842 };
1843
1844 PredictiveSearchComponent.prototype._navigateOption = function(
1845 evt,
1846 direction
1847 ) {
1848 var currentOption = this._getSelectedOption();
1849
1850 if (!currentOption) {
1851 var firstOption = this.nodes.result.querySelector(
1852 this.selectors.searchResult
1853 );
1854 firstOption.classList.add(this.classes.itemSelected);
1855 this._updateAccessibilityAttributesAfterSelectingElement(
1856 null,
1857 firstOption
1858 );
1859 } else {
1860 if (direction === 'DOWN') {
1861 var nextOption = currentOption.nextElementSibling;
1862 if (nextOption) {
1863 currentOption.classList.remove(this.classes.itemSelected);
1864 nextOption.classList.add(this.classes.itemSelected);
1865 this._updateAccessibilityAttributesAfterSelectingElement(
1866 currentOption,
1867 nextOption
1868 );
1869 }
1870 } else {
1871 var previousOption = currentOption.previousElementSibling;
1872 if (previousOption) {
1873 currentOption.classList.remove(this.classes.itemSelected);
1874 previousOption.classList.add(this.classes.itemSelected);
1875 this._updateAccessibilityAttributesAfterSelectingElement(
1876 currentOption,
1877 previousOption
1878 );
1879 }
1880 }
1881 }
1882 };
1883
1884 PredictiveSearchComponent.prototype._getSelectedOption = function() {
1885 return this.nodes.result.querySelector('.' + this.classes.itemSelected);
1886 };
1887
1888 PredictiveSearchComponent.prototype._selectOption = function() {
1889 var selectedOption = this._getSelectedOption();
1890
1891 if (selectedOption) {
1892 selectedOption.querySelector('a, button').click();
1893 }
1894 };
1895
1896 PredictiveSearchComponent.prototype._search = function() {
1897 var newSearchKeyword = this.nodes.input.value;
1898
1899 if (this._searchKeyword === newSearchKeyword) {
1900 return;
1901 }
1902
1903 clearTimeout(this._latencyTimer);
1904 this._latencyTimer = setTimeout(
1905 function() {
1906 this.results.isLoading = true;
1907
1908 // Annonuce that we're loading the results
1909 this._announceLoadingState();
1910
1911 this.nodes.result.classList.add(this.classes.visibleVariant);
1912 // NOTE: We could benifit in using DOMPurify.
1913 // https://github.com/cure53/DOMPurify
1914 this.nodes.result.innerHTML = this.resultTemplateFct(this.results);
1915 }.bind(this),
1916 500
1917 );
1918
1919 this.predictiveSearch.query(newSearchKeyword);
1920 this._setKeyword(newSearchKeyword);
1921 };
1922
1923 PredictiveSearchComponent.prototype._handlePredictiveSearchSuccess = function(
1924 json
1925 ) {
1926 clearTimeout(this._latencyTimer);
1927 this.results = json.resources.results;
1928
1929 this.results.isLoading = false;
1930 this.results.products = this.results.products.slice(
1931 0,
1932 this.numberOfResults
1933 );
1934 this.results.canLoadMore =
1935 this.numberOfResults <= this.results.products.length;
1936 this.results.searchQuery = this.nodes.input.value;
1937
1938 if (this.results.products.length > 0 || this.results.searchQuery) {
1939 this.nodes.result.innerHTML = this.resultTemplateFct(this.results);
1940 this._announceNumberOfResultsFound(this.results);
1941 this.open();
1942 } else {
1943 this.nodes.result.innerHTML = '';
1944
1945 this._closeOnNoResults();
1946 }
1947 };
1948
1949 PredictiveSearchComponent.prototype._handlePredictiveSearchError = function() {
1950 clearTimeout(this._latencyTimer);
1951 this.nodes.result.innerHTML = '';
1952
1953 this._closeOnNoResults();
1954 };
1955
1956 PredictiveSearchComponent.prototype._closeOnNoResults = function() {
1957 if (this.nodes) {
1958 this.nodes.result.classList.remove(this.classes.visibleVariant);
1959 }
1960
1961 this.isResultVisible = false;
1962 };
1963
1964 PredictiveSearchComponent.prototype._setKeyword = function(keyword) {
1965 this._searchKeyword = keyword;
1966 };
1967
1968 PredictiveSearchComponent.prototype._toggleClearButtonVisibility = function() {
1969 if (!this.nodes.reset) {
1970 return;
1971 }
1972
1973 if (this.nodes.input.value.length > 0) {
1974 this.nodes.reset.classList.add(this.classes.clearButtonVisible);
1975 } else {
1976 this.nodes.reset.classList.remove(this.classes.clearButtonVisible);
1977 }
1978 };
1979
1980 /**
1981 * Public methods
1982 */
1983 PredictiveSearchComponent.prototype.open = function() {
1984 if (this.isResultVisible) {
1985 return;
1986 }
1987
1988 if (isFunction(this.callbacks.onBeforeOpen)) {
1989 var returnedValue = this.callbacks.onBeforeOpen(this.nodes);
1990 if (isBoolean(returnedValue) && !returnedValue) {
1991 return false;
1992 }
1993 }
1994
1995 this.nodes.result.classList.add(this.classes.visibleVariant);
1996 this.nodes.input.setAttribute('aria-expanded', true);
1997 this.isResultVisible = true;
1998
1999 if (isFunction(this.callbacks.onOpen)) {
2000 return this.callbacks.onOpen(this.nodes) || true;
2001 }
2002
2003 return true;
2004 };
2005
2006 PredictiveSearchComponent.prototype.close = function() {
2007 if (!this.isResultVisible) {
2008 return true;
2009 }
2010
2011 if (isFunction(this.callbacks.onBeforeClose)) {
2012 var returnedValue = this.callbacks.onBeforeClose(this.nodes);
2013 if (isBoolean(returnedValue) && !returnedValue) {
2014 return false;
2015 }
2016 }
2017
2018 if (this.nodes) {
2019 this.nodes.result.classList.remove(this.classes.visibleVariant);
2020 }
2021
2022 this.nodes.input.setAttribute('aria-expanded', false);
2023 this._clearAriaActiveDescendant();
2024 this._setKeyword('');
2025
2026 if (isFunction(this.callbacks.onClose)) {
2027 this.callbacks.onClose(this.nodes);
2028 }
2029
2030 this.isResultVisible = false;
2031 this.results = {};
2032
2033 return true;
2034 };
2035
2036 PredictiveSearchComponent.prototype.destroy = function() {
2037 this.close();
2038
2039 if (isFunction(this.callbacks.onBeforeDestroy)) {
2040 var returnedValue = this.callbacks.onBeforeDestroy(this.nodes);
2041 if (isBoolean(returnedValue) && !returnedValue) {
2042 return false;
2043 }
2044 }
2045
2046 this.nodes.result.classList.remove(this.classes.visibleVariant);
2047 removeInputAttributes(this.nodes.input);
2048 this._removeInputEventListeners();
2049 this._removeBodyEventListener();
2050 this._removeAccessibilityAnnouncer();
2051 this._removeClearButtonEventListener();
2052
2053 if (isFunction(this.callbacks.onDestroy)) {
2054 this.callbacks.onDestroy(this.nodes);
2055 }
2056
2057 return true;
2058 };
2059
2060 PredictiveSearchComponent.prototype.clearAndClose = function() {
2061 this.nodes.input.value = '';
2062 this.close();
2063 };
2064
2065 /**
2066 * Utilities
2067 */
2068 function getTypeOf(value) {
2069 return Object.prototype.toString.call(value);
2070 }
2071
2072 function isString(value) {
2073 return getTypeOf(value) === '[object String]';
2074 }
2075
2076 function isBoolean(value) {
2077 return getTypeOf(value) === '[object Boolean]';
2078 }
2079
2080 function isFunction(value) {
2081 return getTypeOf(value) === '[object Function]';
2082 }
2083
2084 return PredictiveSearchComponent;
2085})(Shopify.theme.PredictiveSearch);
2086
2087window.theme = window.theme || {};
2088
2089theme.TouchEvents = function TouchEvents(element, options) {
2090 this.axis;
2091 this.checkEvents = [];
2092 this.eventHandlers = {};
2093 this.eventModel = {};
2094 this.events = [
2095 ['touchstart', 'touchmove', 'touchend', 'touchcancel'],
2096 ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'],
2097 ['mousedown', 'mousemove', 'mouseup']
2098 ];
2099 this.eventType;
2100 this.difference = {};
2101 this.direction;
2102 this.start = {};
2103
2104 this.element = element;
2105 this.options = Object.assign(
2106 {},
2107 {
2108 dragThreshold: 10,
2109 start: function() {}, // eslint-disable-line
2110 move: function() {}, // eslint-disable-line
2111 end: function() {} // eslint-disable-line
2112 },
2113 options
2114 );
2115
2116 this.checkEvents = this._getCheckEvents();
2117 this.eventModel = this._getEventModel();
2118
2119 this._setupEventHandlers();
2120};
2121
2122theme.TouchEvents.prototype = Object.assign({}, theme.TouchEvents.prototype, {
2123 destroy: function() {
2124 this.element.removeEventListener(
2125 'dragstart',
2126 this.eventHandlers.preventDefault
2127 );
2128
2129 this.element.removeEventListener(
2130 this.events[this.eventModel][0],
2131 this.eventHandlers.touchStart
2132 );
2133
2134 if (!this.eventModel) {
2135 this.element.removeEventListener(
2136 this.events[2][0],
2137 this.eventHandlers.touchStart
2138 );
2139 }
2140
2141 this.element.removeEventListener('click', this.eventHandlers.preventClick);
2142 },
2143
2144 _setupEventHandlers: function() {
2145 this.eventHandlers.preventDefault = this._preventDefault.bind(this);
2146 this.eventHandlers.preventClick = this._preventClick.bind(this);
2147 this.eventHandlers.touchStart = this._touchStart.bind(this);
2148 this.eventHandlers.touchMove = this._touchMove.bind(this);
2149 this.eventHandlers.touchEnd = this._touchEnd.bind(this);
2150
2151 // Prevent element from dragging when using mouse
2152 this.element.addEventListener(
2153 'dragstart',
2154 this.eventHandlers.preventDefault
2155 );
2156
2157 // Bind the touchstart/pointerdown event
2158 this.element.addEventListener(
2159 this.events[this.eventModel][0],
2160 this.eventHandlers.touchStart
2161 );
2162
2163 // Bind mousedown if necessary
2164 if (!this.eventModel) {
2165 this.element.addEventListener(
2166 this.events[2][0],
2167 this.eventHandlers.touchStart
2168 );
2169 }
2170
2171 // No clicking during touch
2172 this.element.addEventListener('click', this.eventHandlers.preventClick);
2173 },
2174
2175 _touchStart: function(event) {
2176 this.eventType = this.eventModel;
2177
2178 if (event.type === 'mousedown' && !this.eventModel) {
2179 this.eventType = 2;
2180 }
2181
2182 if (this.checkEvents[this.eventType](event)) return;
2183 if (this.eventType) this._preventDefault(event);
2184
2185 document.addEventListener(
2186 this.events[this.eventType][1],
2187 this.eventHandlers.touchMove
2188 );
2189
2190 document.addEventListener(
2191 this.events[this.eventType][2],
2192 this.eventHandlers.touchEnd
2193 );
2194
2195 if (this.eventType < 2) {
2196 document.addEventListener(
2197 this.events[this.eventType][3],
2198 this.eventHandlers.touchEnd
2199 );
2200 }
2201
2202 this.start = {
2203 xPosition: this.eventType ? event.clientX : event.touches[0].clientX,
2204 yPosition: this.eventType ? event.clientY : event.touches[0].clientY,
2205 time: new Date().getTime()
2206 };
2207
2208 // Ensure we empty out the this.difference object
2209 Object.keys(this.difference).forEach(
2210 function(key) {
2211 delete this.difference[key];
2212 }.bind(this)
2213 );
2214
2215 this.options.start(event);
2216 },
2217
2218 _touchMove: function(event) {
2219 this.difference = this._getDifference(event);
2220
2221 // Prevent document from scrolling during swipe gesture
2222 document['on' + this.events[this.eventType][1]] = function(event) {
2223 this._preventDefault(event);
2224 }.bind(this);
2225
2226 // Get the direction user is dragging
2227 if (!this.axis) {
2228 if (this.options.dragThreshold < Math.abs(this.difference.xPosition)) {
2229 this.axis = 'xPosition';
2230 } else if (
2231 this.options.dragThreshold < Math.abs(this.difference.yPosition)
2232 ) {
2233 this.axis = 'yPosition';
2234 } else {
2235 this.axis = false;
2236 }
2237 } else if (this.axis === 'xPosition') {
2238 this.direction = this.difference.xPosition < 0 ? 'left' : 'right';
2239 } else if (this.axis === 'yPosition') {
2240 this.direction = this.difference.yPosition < 0 ? 'up' : 'down';
2241 }
2242
2243 this.options.move(event, this.direction, this.difference);
2244 },
2245
2246 _touchEnd: function(event) {
2247 document.removeEventListener(
2248 this.events[this.eventType][1],
2249 this.eventHandlers.touchMove
2250 );
2251
2252 document.removeEventListener(
2253 this.events[this.eventType][2],
2254 this.eventHandlers.touchEnd
2255 );
2256
2257 if (this.eventType < 2) {
2258 document.removeEventListener(
2259 this.events[this.eventType][3],
2260 this.eventHandlers.touchEnd
2261 );
2262 }
2263
2264 // Re-enable document scrolling
2265 document['on' + this.events[this.eventType][1]] = function() {
2266 return true;
2267 };
2268
2269 this.options.end(event, this.direction, this.difference);
2270 this.axis = false;
2271 },
2272
2273 _getDifference: function(event) {
2274 return {
2275 xPosition:
2276 (this.eventType ? event.clientX : event.touches[0].clientX) -
2277 this.start.xPosition,
2278 yPosition:
2279 (this.eventType ? event.clientY : event.touches[0].clientY) -
2280 this.start.yPosition,
2281 time: new Date().getTime() - this.start.time
2282 };
2283 },
2284
2285 _getCheckEvents: function() {
2286 return [
2287 // Touch events
2288 function(event) {
2289 // Skip the event if it's a multi-touch or pinch move
2290 return (
2291 (event.touches && event.touches.length > 1) ||
2292 (event.scale && event.scale !== 1)
2293 );
2294 },
2295 // Pointer events
2296 function(event) {
2297 // Skip it, if:
2298 // 1. The event is not primary (other pointers during multi-touch),
2299 // 2. Left mouse button is not pressed,
2300 // 3. Event is not a touch event
2301 return (
2302 !event.isPrimary ||
2303 (event.buttons && event.buttons !== 1) ||
2304 (event.pointerType !== 'touch' && event.pointerType !== 'pen')
2305 );
2306 },
2307 // Mouse events
2308 function(event) {
2309 // Skip the event if left mouse button is not pressed
2310 return event.buttons && event.buttons !== 1;
2311 }
2312 ];
2313 },
2314
2315 _getEventModel: function() {
2316 return window.navigator.pointerEnabled ? 1 : 0;
2317 },
2318
2319 _preventDefault: function(event) {
2320 event.preventDefault ? event.preventDefault() : (event.returnValue = false);
2321 },
2322
2323 _preventClick: function(event) {
2324 if (Math.abs(this.difference.xPosition) > this.options.dragThreshold) {
2325 this._preventDefault(event);
2326 }
2327 }
2328});
2329
2330
2331/* ================ GLOBAL ================ */
2332/*============================================================================
2333 Drawer modules
2334==============================================================================*/
2335theme.Drawers = (function() {
2336 function Drawer(id, position, options) {
2337 var DEFAULT_OPEN_CLASS = 'js-drawer-open';
2338 var DEFAULT_CLOSE_CLASS = 'js-drawer-close';
2339
2340 var defaults = {
2341 selectors: {
2342 openVariant: '.' + DEFAULT_OPEN_CLASS + '-' + position,
2343 close: '.' + DEFAULT_CLOSE_CLASS
2344 },
2345 classes: {
2346 open: DEFAULT_OPEN_CLASS,
2347 openVariant: DEFAULT_OPEN_CLASS + '-' + position
2348 },
2349 withPredictiveSearch: false
2350 };
2351
2352 this.nodes = {
2353 parents: [document.documentElement, document.body],
2354 page: document.getElementById('PageContainer')
2355 };
2356
2357 this.eventHandlers = {};
2358
2359 this.config = Object.assign({}, defaults, options);
2360 this.position = position;
2361 this.drawer = document.getElementById(id);
2362
2363 if (!this.drawer) {
2364 return false;
2365 }
2366
2367 this.drawerIsOpen = false;
2368 this.init();
2369 }
2370
2371 Drawer.prototype.init = function() {
2372 document
2373 .querySelector(this.config.selectors.openVariant)
2374 .addEventListener('click', this.open.bind(this));
2375 this.drawer
2376 .querySelector(this.config.selectors.close)
2377 .addEventListener('click', this.close.bind(this));
2378 };
2379
2380 Drawer.prototype.open = function(evt) {
2381 // Keep track if drawer was opened from a click, or called by another function
2382 var externalCall = false;
2383
2384 // Prevent following href if link is clicked
2385 if (evt) {
2386 evt.preventDefault();
2387 } else {
2388 externalCall = true;
2389 }
2390
2391 // Without this, the drawer opens, the click event bubbles up to nodes.page
2392 // which closes the drawer.
2393 if (evt && evt.stopPropagation) {
2394 evt.stopPropagation();
2395 // save the source of the click, we'll focus to this on close
2396 this.activeSource = evt.currentTarget;
2397 }
2398
2399 if (this.drawerIsOpen && !externalCall) {
2400 return this.close();
2401 }
2402
2403 // Add is-transitioning class to moved elements on open so drawer can have
2404 // transition for close animation
2405 if (!this.config.withPredictiveSearch) {
2406 theme.Helpers.prepareTransition(this.drawer);
2407 }
2408
2409 this.nodes.parents.forEach(
2410 function(parent) {
2411 parent.classList.add(
2412 this.config.classes.open,
2413 this.config.classes.openVariant
2414 );
2415 }.bind(this)
2416 );
2417
2418 this.drawerIsOpen = true;
2419
2420 // Run function when draw opens if set
2421 if (
2422 this.config.onDrawerOpen &&
2423 typeof this.config.onDrawerOpen === 'function'
2424 ) {
2425 if (!externalCall) {
2426 this.config.onDrawerOpen();
2427 }
2428 }
2429
2430 if (this.activeSource && this.activeSource.hasAttribute('aria-expanded')) {
2431 this.activeSource.setAttribute('aria-expanded', 'true');
2432 }
2433
2434 // Set focus on drawer
2435 var trapFocusConfig = {
2436 container: this.drawer
2437 };
2438
2439 if (this.config.elementToFocusOnOpen) {
2440 trapFocusConfig.elementToFocus = this.config.elementToFocusOnOpen;
2441 }
2442
2443 slate.a11y.trapFocus(trapFocusConfig);
2444
2445 this.bindEvents();
2446
2447 return this;
2448 };
2449
2450 Drawer.prototype.close = function() {
2451 if (!this.drawerIsOpen) {
2452 // don't close a closed drawer
2453 return;
2454 }
2455
2456 // deselect any focused form elements
2457 document.activeElement.dispatchEvent(
2458 new CustomEvent('blur', { bubbles: true, cancelable: true })
2459 );
2460
2461 // Ensure closing transition is applied to moved elements, like the nav
2462 if (!this.config.withPredictiveSearch) {
2463 theme.Helpers.prepareTransition(this.drawer);
2464 }
2465
2466 this.nodes.parents.forEach(
2467 function(parent) {
2468 parent.classList.remove(
2469 this.config.classes.open,
2470 this.config.classes.openVariant
2471 );
2472 }.bind(this)
2473 );
2474
2475 if (this.activeSource && this.activeSource.hasAttribute('aria-expanded')) {
2476 this.activeSource.setAttribute('aria-expanded', 'false');
2477 }
2478
2479 this.drawerIsOpen = false;
2480
2481 // Remove focus on drawer
2482 slate.a11y.removeTrapFocus({
2483 container: this.drawer
2484 });
2485
2486 this.unbindEvents();
2487
2488 // Run function when draw closes if set
2489 if (
2490 this.config.onDrawerClose &&
2491 typeof this.config.onDrawerClose === 'function'
2492 ) {
2493 this.config.onDrawerClose();
2494 }
2495 };
2496
2497 Drawer.prototype.bindEvents = function() {
2498 this.eventHandlers.drawerKeyupHandler = function(evt) {
2499 // close on 'esc' keypress
2500 if (evt.keyCode === 27) {
2501 this.close();
2502 return false;
2503 } else {
2504 return true;
2505 }
2506 }.bind(this);
2507
2508 this.eventHandlers.drawerTouchmoveHandler = function() {
2509 return false;
2510 };
2511
2512 this.eventHandlers.drawerClickHandler = function() {
2513 this.close();
2514 return false;
2515 }.bind(this);
2516
2517 // Add event listener to document body
2518 document.body.addEventListener(
2519 'keyup',
2520 this.eventHandlers.drawerKeyupHandler
2521 );
2522
2523 // Lock scrolling on mobile
2524 this.nodes.page.addEventListener(
2525 'touchmove',
2526 this.eventHandlers.drawerTouchmoveHandler
2527 );
2528
2529 this.nodes.page.addEventListener(
2530 'click',
2531 this.eventHandlers.drawerClickHandler
2532 );
2533 };
2534
2535 Drawer.prototype.unbindEvents = function() {
2536 this.nodes.page.removeEventListener(
2537 'touchmove',
2538 this.eventHandlers.drawerTouchmoveHandler
2539 );
2540 this.nodes.page.removeEventListener(
2541 'click',
2542 this.eventHandlers.drawerClickHandler
2543 );
2544 document.body.removeEventListener(
2545 'keyup',
2546 this.eventHandlers.drawerKeyupHandler
2547 );
2548 };
2549
2550 return Drawer;
2551})();
2552
2553theme.Helpers = (function() {
2554 var touchDevice = false;
2555
2556 var classes = {
2557 preventScrolling: 'prevent-scrolling'
2558 };
2559
2560 var scrollPosition = window.pageYOffset;
2561
2562 function setTouch() {
2563 touchDevice = true;
2564 }
2565
2566 function isTouch() {
2567 return touchDevice;
2568 }
2569
2570 function enableScrollLock() {
2571 scrollPosition = window.pageYOffset;
2572 document.body.style.top = '-' + scrollPosition + 'px';
2573 document.body.classList.add(classes.preventScrolling);
2574 }
2575
2576 function disableScrollLock() {
2577 document.body.classList.remove(classes.preventScrolling);
2578 document.body.style.removeProperty('top');
2579 window.scrollTo(0, scrollPosition);
2580 }
2581
2582 function debounce(func, wait, immediate) {
2583 var timeout;
2584
2585 return function() {
2586 var context = this,
2587 args = arguments;
2588
2589 var later = function() {
2590 timeout = null;
2591 if (!immediate) func.apply(context, args);
2592 };
2593
2594 var callNow = immediate && !timeout;
2595 clearTimeout(timeout);
2596 timeout = setTimeout(later, wait);
2597 if (callNow) func.apply(context, args);
2598 };
2599 }
2600
2601 function getScript(source, beforeEl) {
2602 return new Promise(function(resolve, reject) {
2603 var script = document.createElement('script');
2604 var prior = beforeEl || document.getElementsByTagName('script')[0];
2605
2606 script.async = true;
2607 script.defer = true;
2608
2609 // eslint-disable-next-line shopify/prefer-early-return
2610 function onloadHander(_, isAbort) {
2611 if (
2612 isAbort ||
2613 !script.readyState ||
2614 /loaded|complete/.test(script.readyState)
2615 ) {
2616 script.onload = null;
2617 script.onreadystatechange = null;
2618 script = undefined;
2619
2620 if (isAbort) {
2621 reject();
2622 } else {
2623 resolve();
2624 }
2625 }
2626 }
2627
2628 script.onload = onloadHander;
2629 script.onreadystatechange = onloadHander;
2630
2631 script.src = source;
2632 prior.parentNode.insertBefore(script, prior);
2633 });
2634 }
2635
2636 /* Based on the prepareTransition by Jonathan Snook */
2637 /* Jonathan Snook - MIT License - https://github.com/snookca/prepareTransition */
2638 function prepareTransition(element) {
2639 element.addEventListener(
2640 'transitionend',
2641 function(event) {
2642 event.currentTarget.classList.remove('is-transitioning');
2643 },
2644 { once: true }
2645 );
2646
2647 var properties = [
2648 'transition-duration',
2649 '-moz-transition-duration',
2650 '-webkit-transition-duration',
2651 '-o-transition-duration'
2652 ];
2653
2654 var duration = 0;
2655
2656 properties.forEach(function(property) {
2657 var computedValue = getComputedStyle(element)[property];
2658
2659 if (computedValue) {
2660 computedValue.replace(/\D/g, '');
2661 duration || (duration = parseFloat(computedValue));
2662 }
2663 });
2664
2665 if (duration !== 0) {
2666 element.classList.add('is-transitioning');
2667 element.offsetWidth;
2668 }
2669 }
2670
2671 /*!
2672 * Serialize all form data into a SearchParams string
2673 * (c) 2020 Chris Ferdinandi, MIT License, https://gomakethings.com
2674 * @param {Node} form The form to serialize
2675 * @return {String} The serialized form data
2676 */
2677 function serialize(form) {
2678 var arr = [];
2679 Array.prototype.slice.call(form.elements).forEach(function(field) {
2680 if (
2681 !field.name ||
2682 field.disabled ||
2683 ['file', 'reset', 'submit', 'button'].indexOf(field.type) > -1
2684 )
2685 return;
2686 if (field.type === 'select-multiple') {
2687 Array.prototype.slice.call(field.options).forEach(function(option) {
2688 if (!option.selected) return;
2689 arr.push(
2690 encodeURIComponent(field.name) +
2691 '=' +
2692 encodeURIComponent(option.value)
2693 );
2694 });
2695 return;
2696 }
2697 if (['checkbox', 'radio'].indexOf(field.type) > -1 && !field.checked)
2698 return;
2699 arr.push(
2700 encodeURIComponent(field.name) + '=' + encodeURIComponent(field.value)
2701 );
2702 });
2703 return arr.join('&');
2704 }
2705 function cookiesEnabled() {
2706 var cookieEnabled = navigator.cookieEnabled;
2707
2708 if (!cookieEnabled) {
2709 document.cookie = 'testcookie';
2710 cookieEnabled = document.cookie.indexOf('testcookie') !== -1;
2711 }
2712
2713 return cookieEnabled;
2714 }
2715
2716 function promiseStylesheet(stylesheet) {
2717 var stylesheetUrl = stylesheet || theme.stylesheet;
2718
2719 if (typeof this.stylesheetPromise === 'undefined') {
2720 this.stylesheetPromise = new Promise(function(resolve) {
2721 var link = document.querySelector('link[href="' + stylesheetUrl + '"]');
2722
2723 if (link.loaded) resolve();
2724
2725 link.addEventListener('load', function() {
2726 setTimeout(resolve, 0);
2727 });
2728 });
2729 }
2730
2731 return this.stylesheetPromise;
2732 }
2733
2734 return {
2735 setTouch: setTouch,
2736 isTouch: isTouch,
2737 enableScrollLock: enableScrollLock,
2738 disableScrollLock: disableScrollLock,
2739 debounce: debounce,
2740 getScript: getScript,
2741 prepareTransition: prepareTransition,
2742 serialize: serialize,
2743 cookiesEnabled: cookiesEnabled,
2744 promiseStylesheet: promiseStylesheet
2745 };
2746})();
2747
2748theme.LibraryLoader = (function() {
2749 var types = {
2750 link: 'link',
2751 script: 'script'
2752 };
2753
2754 var status = {
2755 requested: 'requested',
2756 loaded: 'loaded'
2757 };
2758
2759 var cloudCdn = 'https://cdn.shopify.com/shopifycloud/';
2760
2761 var libraries = {
2762 youtubeSdk: {
2763 tagId: 'youtube-sdk',
2764 src: 'https://www.youtube.com/iframe_api',
2765 type: types.script
2766 },
2767 plyrShopifyStyles: {
2768 tagId: 'plyr-shopify-styles',
2769 src: cloudCdn + 'shopify-plyr/v1.0/shopify-plyr.css',
2770 type: types.link
2771 },
2772 modelViewerUiStyles: {
2773 tagId: 'shopify-model-viewer-ui-styles',
2774 src: cloudCdn + 'model-viewer-ui/assets/v1.0/model-viewer-ui.css',
2775 type: types.link
2776 }
2777 };
2778
2779 function load(libraryName, callback) {
2780 var library = libraries[libraryName];
2781
2782 if (!library) return;
2783 if (library.status === status.requested) return;
2784
2785 callback = callback || function() {};
2786 if (library.status === status.loaded) {
2787 callback();
2788 return;
2789 }
2790
2791 library.status = status.requested;
2792
2793 var tag;
2794
2795 switch (library.type) {
2796 case types.script:
2797 tag = createScriptTag(library, callback);
2798 break;
2799 case types.link:
2800 tag = createLinkTag(library, callback);
2801 break;
2802 }
2803
2804 tag.id = library.tagId;
2805 library.element = tag;
2806
2807 var firstScriptTag = document.getElementsByTagName(library.type)[0];
2808 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
2809 }
2810
2811 function createScriptTag(library, callback) {
2812 var tag = document.createElement('script');
2813 tag.src = library.src;
2814 tag.addEventListener('load', function() {
2815 library.status = status.loaded;
2816 callback();
2817 });
2818 return tag;
2819 }
2820
2821 function createLinkTag(library, callback) {
2822 var tag = document.createElement('link');
2823 tag.href = library.src;
2824 tag.rel = 'stylesheet';
2825 tag.type = 'text/css';
2826 tag.addEventListener('load', function() {
2827 library.status = status.loaded;
2828 callback();
2829 });
2830 return tag;
2831 }
2832
2833 return {
2834 load: load
2835 };
2836})();
2837
2838
2839/* ================ MODULES ================ */
2840window.theme = window.theme || {};
2841
2842theme.Header = (function() {
2843 var selectors = {
2844 body: 'body',
2845 navigation: '#AccessibleNav',
2846 siteNavHasDropdown: '[data-has-dropdowns]',
2847 siteNavChildLinks: '.site-nav__child-link',
2848 siteNavActiveDropdown: '.site-nav--active-dropdown',
2849 siteNavHasCenteredDropdown: '.site-nav--has-centered-dropdown',
2850 siteNavCenteredDropdown: '.site-nav__dropdown--centered',
2851 siteNavLinkMain: '.site-nav__link--main',
2852 siteNavChildLink: '.site-nav__link--last',
2853 siteNavDropdown: '.site-nav__dropdown',
2854 siteHeader: '.site-header'
2855 };
2856
2857 var config = {
2858 activeClass: 'site-nav--active-dropdown',
2859 childLinkClass: 'site-nav__child-link',
2860 rightDropdownClass: 'site-nav__dropdown--right',
2861 leftDropdownClass: 'site-nav__dropdown--left'
2862 };
2863
2864 var cache = {};
2865
2866 function init() {
2867 cacheSelectors();
2868 styleDropdowns(document.querySelectorAll(selectors.siteNavHasDropdown));
2869 positionFullWidthDropdowns();
2870
2871 cache.parents.forEach(function(element) {
2872 element.addEventListener('click', submenuParentClickHandler);
2873 });
2874
2875 // check when we're leaving a dropdown and close the active dropdown
2876 cache.siteNavChildLink.forEach(function(element) {
2877 element.addEventListener('focusout', submenuFocusoutHandler);
2878 });
2879
2880 cache.topLevel.forEach(function(element) {
2881 element.addEventListener('focus', hideDropdown);
2882 });
2883
2884 cache.subMenuLinks.forEach(function(element) {
2885 element.addEventListener('click', stopImmediatePropagation);
2886 });
2887
2888 window.addEventListener('resize', resizeHandler);
2889 }
2890
2891 function stopImmediatePropagation(event) {
2892 event.stopImmediatePropagation();
2893 }
2894
2895 function cacheSelectors() {
2896 var navigation = document.querySelector(selectors.navigation);
2897
2898 cache = {
2899 nav: navigation,
2900 topLevel: document.querySelectorAll(selectors.siteNavLinkMain),
2901 parents: navigation.querySelectorAll(selectors.siteNavHasDropdown),
2902 subMenuLinks: document.querySelectorAll(selectors.siteNavChildLinks),
2903 activeDropdown: document.querySelector(selectors.siteNavActiveDropdown),
2904 siteHeader: document.querySelector(selectors.siteHeader),
2905 siteNavChildLink: document.querySelectorAll(selectors.siteNavChildLink)
2906 };
2907 }
2908
2909 function showDropdown(element) {
2910 element.classList.add(config.activeClass);
2911
2912 if (cache.activeDropdown) hideDropdown();
2913
2914 cache.activeDropdown = element;
2915
2916 element
2917 .querySelector(selectors.siteNavLinkMain)
2918 .setAttribute('aria-expanded', 'true');
2919
2920 setTimeout(function() {
2921 window.addEventListener('keyup', keyUpHandler);
2922 document.body.addEventListener('click', hideDropdown);
2923 }, 250);
2924 }
2925
2926 function hideDropdown() {
2927 if (!cache.activeDropdown) return;
2928
2929 cache.activeDropdown
2930 .querySelector(selectors.siteNavLinkMain)
2931 .setAttribute('aria-expanded', 'false');
2932 cache.activeDropdown.classList.remove(config.activeClass);
2933
2934 cache.activeDropdown = document.querySelector(
2935 selectors.siteNavActiveDropdown
2936 );
2937
2938 window.removeEventListener('keyup', keyUpHandler);
2939 document.body.removeEventListener('click', hideDropdown);
2940 }
2941
2942 function styleDropdowns(dropdownListItems) {
2943 dropdownListItems.forEach(function(item) {
2944 var dropdownLi = item.querySelector(selectors.siteNavDropdown);
2945
2946 if (!dropdownLi) return;
2947
2948 if (isRightOfLogo(item)) {
2949 dropdownLi.classList.remove(config.leftDropdownClass);
2950 dropdownLi.classList.add(config.rightDropdownClass);
2951 } else {
2952 dropdownLi.classList.remove(config.rightDropdownClass);
2953 dropdownLi.classList.add(config.leftDropdownClass);
2954 }
2955 });
2956 }
2957
2958 function isRightOfLogo(item) {
2959 var rect = item.getBoundingClientRect();
2960 var win = item.ownerDocument.defaultView;
2961 var leftOffset = rect.left + win.pageXOffset;
2962
2963 var headerWidth = Math.floor(cache.siteHeader.offsetWidth) / 2;
2964 return leftOffset > headerWidth;
2965 }
2966
2967 function positionFullWidthDropdowns() {
2968 document
2969 .querySelectorAll(selectors.siteNavHasCenteredDropdown)
2970 .forEach(function(el) {
2971 var fullWidthDropdown = el.querySelector(
2972 selectors.siteNavCenteredDropdown
2973 );
2974
2975 var fullWidthDropdownOffset = el.offsetTop + 41;
2976 fullWidthDropdown.style.top = fullWidthDropdownOffset + 'px';
2977 });
2978 }
2979
2980 function keyUpHandler(event) {
2981 if (event.keyCode === 27) hideDropdown();
2982 }
2983
2984 function resizeHandler() {
2985 adjustStyleAndPosition();
2986 }
2987
2988 function submenuParentClickHandler(event) {
2989 var element = event.currentTarget;
2990
2991 element.classList.contains(config.activeClass)
2992 ? hideDropdown()
2993 : showDropdown(element);
2994 }
2995
2996 function submenuFocusoutHandler() {
2997 setTimeout(function() {
2998 if (
2999 document.activeElement.classList.contains(config.childLinkClass) ||
3000 !cache.activeDropdown
3001 ) {
3002 return;
3003 }
3004
3005 hideDropdown();
3006 });
3007 }
3008
3009 var adjustStyleAndPosition = theme.Helpers.debounce(function() {
3010 styleDropdowns(document.querySelectorAll(selectors.siteNavHasDropdown));
3011 positionFullWidthDropdowns();
3012 }, 50);
3013
3014 function unload() {
3015 cache.topLevel.forEach(function(element) {
3016 element.removeEventListener('focus', hideDropdown);
3017 });
3018
3019 cache.subMenuLinks.forEach(function(element) {
3020 element.removeEventListener('click', stopImmediatePropagation);
3021 });
3022
3023 cache.parents.forEach(function(element) {
3024 element.removeEventListener('click', submenuParentClickHandler);
3025 });
3026
3027 cache.siteNavChildLink.forEach(function(element) {
3028 element.removeEventListener('focusout', submenuFocusoutHandler);
3029 });
3030
3031 window.removeEventListener('resize', resizeHandler);
3032 window.removeEventListener('keyup', keyUpHandler);
3033 document.body.removeEventListener('click', hideDropdown);
3034 }
3035
3036 return {
3037 init: init,
3038 unload: unload
3039 };
3040})();
3041
3042window.theme = window.theme || {};
3043
3044theme.MobileNav = (function() {
3045 var classes = {
3046 mobileNavOpenIcon: 'mobile-nav--open',
3047 mobileNavCloseIcon: 'mobile-nav--close',
3048 navLinkWrapper: 'mobile-nav__item',
3049 navLink: 'mobile-nav__link',
3050 subNavLink: 'mobile-nav__sublist-link',
3051 return: 'mobile-nav__return-btn',
3052 subNavActive: 'is-active',
3053 subNavClosing: 'is-closing',
3054 navOpen: 'js-menu--is-open',
3055 subNavShowing: 'sub-nav--is-open',
3056 thirdNavShowing: 'third-nav--is-open',
3057 subNavToggleBtn: 'js-toggle-submenu'
3058 };
3059
3060 var cache = {};
3061 var isTransitioning;
3062 var activeSubNav;
3063 var activeTrigger;
3064 var menuLevel = 1;
3065 var mediumUpQuery = '(min-width: ' + theme.breakpoints.medium + 'px)';
3066 var mql = window.matchMedia(mediumUpQuery);
3067
3068 function init() {
3069 cacheSelectors();
3070
3071 if (cache.mobileNavToggle) {
3072 cache.mobileNavToggle.addEventListener('click', toggleMobileNav);
3073 }
3074
3075 cache.subNavToggleBtns.forEach(function(element) {
3076 element.addEventListener('click', toggleSubNav);
3077 });
3078
3079 mql.addListener(initBreakpoints);
3080 }
3081
3082 function initBreakpoints() {
3083 if (
3084 mql.matches &&
3085 cache.mobileNavContainer.classList.contains(classes.navOpen)
3086 ) {
3087 closeMobileNav();
3088 }
3089 }
3090
3091 function toggleMobileNav() {
3092 var mobileNavIsOpen = cache.mobileNavToggle.classList.contains(
3093 classes.mobileNavCloseIcon
3094 );
3095
3096 if (mobileNavIsOpen) {
3097 closeMobileNav();
3098 } else {
3099 openMobileNav();
3100 }
3101 }
3102
3103 function cacheSelectors() {
3104 cache = {
3105 pageContainer: document.querySelector('#PageContainer'),
3106 siteHeader: document.querySelector('.site-header'),
3107 mobileNavToggle: document.querySelector('.js-mobile-nav-toggle'),
3108 mobileNavContainer: document.querySelector('.mobile-nav-wrapper'),
3109 mobileNav: document.querySelector('#MobileNav'),
3110 sectionHeader: document.querySelector('#shopify-section-header'),
3111 subNavToggleBtns: document.querySelectorAll('.' + classes.subNavToggleBtn)
3112 };
3113 }
3114
3115 function openMobileNav() {
3116 var translateHeaderHeight = cache.siteHeader.offsetHeight;
3117
3118 theme.Helpers.prepareTransition(cache.mobileNavContainer);
3119 cache.mobileNavContainer.classList.add(classes.navOpen);
3120
3121 cache.mobileNavContainer.style.transform =
3122 'translateY(' + translateHeaderHeight + 'px)';
3123
3124 cache.pageContainer.style.transform =
3125 'translate3d(0, ' + cache.mobileNavContainer.scrollHeight + 'px, 0)';
3126
3127 slate.a11y.trapFocus({
3128 container: cache.sectionHeader,
3129 elementToFocus: cache.mobileNavToggle
3130 });
3131
3132 cache.mobileNavToggle.classList.add(classes.mobileNavCloseIcon);
3133 cache.mobileNavToggle.classList.remove(classes.mobileNavOpenIcon);
3134 cache.mobileNavToggle.setAttribute('aria-expanded', true);
3135
3136 window.addEventListener('keyup', keyUpHandler);
3137 }
3138
3139 function keyUpHandler(event) {
3140 if (event.which === 27) {
3141 closeMobileNav();
3142 }
3143 }
3144
3145 function closeMobileNav() {
3146 theme.Helpers.prepareTransition(cache.mobileNavContainer);
3147 cache.mobileNavContainer.classList.remove(classes.navOpen);
3148 cache.mobileNavContainer.style.transform = 'translateY(-100%)';
3149 cache.pageContainer.setAttribute('style', '');
3150
3151 slate.a11y.trapFocus({
3152 container: document.querySelector('html'),
3153 elementToFocus: document.body
3154 });
3155
3156 cache.mobileNavContainer.addEventListener(
3157 'transitionend',
3158 mobileNavRemoveTrapFocus,
3159 { once: true }
3160 );
3161
3162 cache.mobileNavToggle.classList.add(classes.mobileNavOpenIcon);
3163 cache.mobileNavToggle.classList.remove(classes.mobileNavCloseIcon);
3164 cache.mobileNavToggle.setAttribute('aria-expanded', false);
3165 cache.mobileNavToggle.focus();
3166
3167 window.removeEventListener('keyup', keyUpHandler);
3168 window.scrollTo(0, 0);
3169 }
3170
3171 function mobileNavRemoveTrapFocus() {
3172 slate.a11y.removeTrapFocus({
3173 container: cache.mobileNav
3174 });
3175 }
3176
3177 function toggleSubNav(event) {
3178 if (isTransitioning) return;
3179
3180 var toggleBtn = event.currentTarget;
3181 var isReturn = toggleBtn.classList.contains(classes.return);
3182
3183 isTransitioning = true;
3184
3185 if (isReturn) {
3186 var subNavToggleBtn = document.querySelectorAll(
3187 '.' + classes.subNavToggleBtn + "[data-level='" + (menuLevel - 1) + "']"
3188 );
3189
3190 subNavToggleBtn.forEach(function(element) {
3191 element.classList.remove(classes.subNavActive);
3192 });
3193
3194 if (activeTrigger) {
3195 activeTrigger.classList.remove(classes.subNavActive);
3196 }
3197 } else {
3198 toggleBtn.classList.add(classes.subNavActive);
3199 }
3200
3201 activeTrigger = toggleBtn;
3202
3203 goToSubnav(toggleBtn.getAttribute('data-target'));
3204 }
3205
3206 function goToSubnav(target) {
3207 var targetMenu = target
3208 ? document.querySelector(
3209 '.mobile-nav__dropdown[data-parent="' + target + '"]'
3210 )
3211 : cache.mobileNav;
3212
3213 menuLevel = targetMenu.dataset.level ? Number(targetMenu.dataset.level) : 1;
3214
3215 if (activeSubNav) {
3216 theme.Helpers.prepareTransition(activeSubNav);
3217 activeSubNav.classList.add(classes.subNavClosing);
3218 }
3219
3220 activeSubNav = targetMenu;
3221
3222 var translateMenuHeight = targetMenu.offsetHeight;
3223
3224 var openNavClass =
3225 menuLevel > 2 ? classes.thirdNavShowing : classes.subNavShowing;
3226
3227 cache.mobileNavContainer.style.height = translateMenuHeight + 'px';
3228 cache.mobileNavContainer.classList.remove(classes.thirdNavShowing);
3229 cache.mobileNavContainer.classList.add(openNavClass);
3230
3231 if (!target) {
3232 cache.mobileNavContainer.classList.remove(
3233 classes.thirdNavShowing,
3234 classes.subNavShowing
3235 );
3236 }
3237
3238 /* if going back to first subnav, focus is on whole header */
3239 var container = menuLevel === 1 ? cache.sectionHeader : targetMenu;
3240
3241 cache.mobileNavContainer.addEventListener(
3242 'transitionend',
3243 trapMobileNavFocus,
3244 { once: true }
3245 );
3246
3247 function trapMobileNavFocus() {
3248 slate.a11y.trapFocus({
3249 container: container
3250 });
3251
3252 cache.mobileNavContainer.removeEventListener(
3253 'transitionend',
3254 trapMobileNavFocus
3255 );
3256
3257 isTransitioning = false;
3258 }
3259
3260 // Match height of subnav
3261 cache.pageContainer.style.transform =
3262 'translateY(' + translateMenuHeight + 'px)';
3263
3264 activeSubNav.classList.remove(classes.subNavClosing);
3265 }
3266
3267 function unload() {
3268 mql.removeListener(initBreakpoints);
3269 }
3270
3271 return {
3272 init: init,
3273 unload: unload,
3274 closeMobileNav: closeMobileNav
3275 };
3276})();
3277
3278window.Modals = (function() {
3279 function Modal(id, name, options) {
3280 var defaults = {
3281 close: '.js-modal-close',
3282 open: '.js-modal-open-' + name,
3283 openClass: 'modal--is-active',
3284 closeModalOnClick: false
3285 };
3286
3287 this.modal = document.getElementById(id);
3288
3289 if (!this.modal) return false;
3290
3291 this.nodes = {
3292 parents: [document.querySelector('html'), document.body]
3293 };
3294
3295 this.config = Object.assign(defaults, options);
3296
3297 this.modalIsOpen = false;
3298
3299 this.focusOnOpen = this.config.focusOnOpen
3300 ? document.getElementById(this.config.focusOnOpen)
3301 : this.modal;
3302
3303 this.openElement = document.querySelector(this.config.open);
3304 this.init();
3305 }
3306
3307 Modal.prototype.init = function() {
3308 this.openElement.addEventListener('click', this.open.bind(this));
3309
3310 this.modal
3311 .querySelector(this.config.close)
3312 .addEventListener('click', this.closeModal.bind(this));
3313 };
3314
3315 Modal.prototype.open = function(evt) {
3316 var self = this;
3317 // Keep track if modal was opened from a click, or called by another function
3318 var externalCall = false;
3319
3320 if (this.modalIsOpen) return;
3321
3322 // Prevent following href if link is clicked
3323 if (evt) {
3324 evt.preventDefault();
3325 } else {
3326 externalCall = true;
3327 }
3328
3329 // Without this, the modal opens, the click event bubbles up
3330 // which closes the modal.
3331 if (evt && evt.stopPropagation) {
3332 evt.stopPropagation();
3333 }
3334
3335 if (this.modalIsOpen && !externalCall) {
3336 this.closeModal();
3337 }
3338
3339 this.modal.classList.add(this.config.openClass);
3340
3341 this.nodes.parents.forEach(function(node) {
3342 node.classList.add(self.config.openClass);
3343 });
3344
3345 this.modalIsOpen = true;
3346
3347 slate.a11y.trapFocus({
3348 container: this.modal,
3349 elementToFocus: this.focusOnOpen
3350 });
3351
3352 this.bindEvents();
3353 };
3354
3355 Modal.prototype.closeModal = function() {
3356 if (!this.modalIsOpen) return;
3357
3358 document.activeElement.blur();
3359
3360 this.modal.classList.remove(this.config.openClass);
3361
3362 var self = this;
3363
3364 this.nodes.parents.forEach(function(node) {
3365 node.classList.remove(self.config.openClass);
3366 });
3367
3368 this.modalIsOpen = false;
3369
3370 slate.a11y.removeTrapFocus({
3371 container: this.modal
3372 });
3373
3374 this.openElement.focus();
3375
3376 this.unbindEvents();
3377 };
3378
3379 Modal.prototype.bindEvents = function() {
3380 this.keyupHandler = this.keyupHandler.bind(this);
3381 this.clickHandler = this.clickHandler.bind(this);
3382 document.body.addEventListener('keyup', this.keyupHandler);
3383 document.body.addEventListener('click', this.clickHandler);
3384 };
3385
3386 Modal.prototype.unbindEvents = function() {
3387 document.body.removeEventListener('keyup', this.keyupHandler);
3388 document.body.removeEventListener('click', this.clickHandler);
3389 };
3390
3391 Modal.prototype.keyupHandler = function(event) {
3392 if (event.keyCode === 27) {
3393 this.closeModal();
3394 }
3395 };
3396
3397 Modal.prototype.clickHandler = function(event) {
3398 if (this.config.closeModalOnClick && !this.modal.contains(event.target)) {
3399 this.closeModal();
3400 }
3401 };
3402
3403 return Modal;
3404})();
3405
3406(function() {
3407 var selectors = {
3408 backButton: '.return-link'
3409 };
3410
3411 var backButton = document.querySelector(selectors.backButton);
3412
3413 if (!document.referrer || !backButton || !window.history.length) {
3414 return;
3415 }
3416
3417 backButton.addEventListener(
3418 'click',
3419 function(evt) {
3420 evt.preventDefault();
3421
3422 var referrerDomain = urlDomain(document.referrer);
3423 var shopDomain = urlDomain(window.location.href);
3424
3425 if (shopDomain === referrerDomain) {
3426 history.back();
3427 }
3428
3429 return false;
3430 },
3431 { once: true }
3432 );
3433
3434 function urlDomain(url) {
3435 var anchor = document.createElement('a');
3436 anchor.ref = url;
3437
3438 return anchor.hostname;
3439 }
3440})();
3441
3442theme.Slideshow = (function() {
3443 var selectors = {
3444 button: '[data-slider-button]',
3445 indicator: '[data-slider-indicator]',
3446 indicators: '[data-slider-indicators]',
3447 pause: '[data-slider-pause]',
3448 slider: '[data-slider]',
3449 sliderItem: '[data-slider-item]',
3450 sliderItemLink: '[data-slider-item-link]',
3451 sliderTrack: '[data-slider-track]',
3452 sliderContainer: '[data-slider-container]'
3453 };
3454
3455 var classes = {
3456 isPaused: 'slideshow__pause--is-paused',
3457 indicator: 'slider-indicators__item',
3458 indicatorActive: 'slick-active',
3459 sliderInitialized: 'slick-initialized',
3460 slideActive: 'slideshow__slide--active',
3461 slideClone: 'slick-cloned'
3462 };
3463
3464 var attributes = {
3465 buttonNext: 'data-slider-button-next'
3466 };
3467
3468 function Slideshow(container, options) {
3469 this.container = container;
3470 this.slider = this.container.querySelector(selectors.slider);
3471
3472 if (!this.slider) return;
3473
3474 this.eventHandlers = {};
3475 this.lastSlide = 0;
3476 this.slideIndex = 0;
3477 this.sliderContainer = null;
3478 this.slides = [];
3479 this.options = Object.assign(
3480 {},
3481 {
3482 autoplay: false,
3483 canUseKeyboardArrows: true,
3484 canUseTouchEvents: false,
3485 slideActiveClass: classes.slideActive,
3486 slideInterval: 0,
3487 slidesToShow: 0,
3488 slidesToScroll: 1,
3489 type: 'fade'
3490 },
3491 options
3492 );
3493
3494 this.sliderContainer = this.slider.querySelector(selectors.sliderContainer);
3495 this.adaptHeight =
3496 this.sliderContainer.getAttribute('data-adapt-height') === 'true';
3497 this.slides = Array.from(
3498 this.sliderContainer.querySelectorAll(selectors.sliderItem)
3499 );
3500 // adding -1 to accomodate Array order
3501 this.lastSlide = this.slides.length - 1;
3502 this.buttons = this.container.querySelectorAll(selectors.button);
3503 this.pause = this.container.querySelector(selectors.pause);
3504 this.indicators = this.container.querySelectorAll(selectors.indicators);
3505
3506 if (this.slides.length <= 1) return;
3507
3508 this.timeout = 250;
3509
3510 if (this.options.autoplay) {
3511 this.startAutoplay();
3512 }
3513
3514 if (this.adaptHeight) {
3515 this.setSlideshowHeight();
3516 }
3517
3518 if (this.options.type === 'slide') {
3519 this.isFirstSlide = false;
3520 this.isLastSlide = false;
3521 this.sliderItemWidthTotal = 0;
3522 this.sliderTrack = this.slider.querySelector(selectors.sliderTrack);
3523 // added setTimeout due to matchMedia calling too early
3524 // which result wrong value when getting dimension from an element
3525 this.sliderItemWidthTotal = 0;
3526 theme.Helpers.promiseStylesheet().then(
3527 function() {
3528 this._setupSlideType();
3529 }.bind(this)
3530 );
3531 } else {
3532 this.setupSlider(0);
3533 }
3534
3535 this._setupEventHandlers();
3536 }
3537
3538 Slideshow.prototype = Object.assign({}, Slideshow.prototype, {
3539 /**
3540 * Moves to the previous slide
3541 */
3542 previousSlide: function() {
3543 this._move();
3544 },
3545
3546 /**
3547 * Moves to the next slide
3548 */
3549 nextSlide: function() {
3550 this._move('next');
3551 },
3552
3553 /**
3554 * Moves to the specified slide
3555 * @param {Number} index - The index of the slide to move to
3556 */
3557 setSlide: function(index) {
3558 this._setPosition(Number(index));
3559 },
3560
3561 /**
3562 * Starts autoplaying the slider if autoplay is enabled
3563 */
3564 startAutoplay: function() {
3565 this.isAutoPlaying = true;
3566
3567 window.clearTimeout(this.autoTimeOut);
3568
3569 this.autoTimeOut = window.setTimeout(
3570 function() {
3571 var nextSlideIndex = this._getNextSlideIndex('next');
3572 this._setPosition(nextSlideIndex);
3573 }.bind(this),
3574 this.options.slideInterval
3575 );
3576 },
3577
3578 /**
3579 * Stops autoplaying the slider if autoplay is enabled
3580 */
3581 stopAutoplay: function() {
3582 this.isAutoPlaying = false;
3583
3584 window.clearTimeout(this.autoTimeOut);
3585 },
3586
3587 /**
3588 * Set active states for sliders and indicators
3589 * @param {index} integer - Slide index to set up slider from
3590 */
3591 setupSlider: function(index) {
3592 this.slideIndex = index;
3593
3594 if (this.indicators.length) {
3595 this._setActiveIndicator(index);
3596 }
3597
3598 this._setupActiveSlide(index);
3599 },
3600
3601 /**
3602 * Removes event listeners, among other things when wanting to destroy the
3603 * slider instance. This method needs to be called manually and will most
3604 * likely be included in a section's onUnload() method.
3605 */
3606 destroy: function() {
3607 if (this.adaptHeight) {
3608 window.removeEventListener('resize', this.eventHandlers.debounceResize);
3609 }
3610
3611 this.container.removeEventListener(
3612 'focus',
3613 this.eventHandlers.focus,
3614 true
3615 );
3616 this.slider.removeEventListener(
3617 'focusin',
3618 this.eventHandlers.focusIn,
3619 true
3620 );
3621 this.slider.removeEventListener(
3622 'focusout',
3623 this.eventHandlers.focusOut,
3624 true
3625 );
3626 this.container.removeEventListener('blur', this.eventHandlers.blur, true);
3627
3628 if (this.buttons) {
3629 this.buttons.forEach(
3630 function(button) {
3631 button.removeEventListener('click', this.eventHandlers.clickButton);
3632 }.bind(this)
3633 );
3634 }
3635
3636 this.indicators.forEach(function(indicatorWrapper) {
3637 indicatorWrapper.childNodes.forEach(function(indicator) {
3638 indicator.firstElementChild.removeEventListener(
3639 'click',
3640 this.eventHandlers.onClickIndicator
3641 );
3642
3643 indicator.firstElementChild.removeEventListener(
3644 'keydown',
3645 this.eventHandlers.onKeydownIndicator
3646 );
3647 }, this);
3648 }, this);
3649
3650 if (this.options.type === 'slide') {
3651 window.removeEventListener(
3652 'resize',
3653 this.eventHandlers.debounceResizeSlideIn
3654 );
3655
3656 if (this.touchEvents && this.options.canUseTouchEvents) {
3657 this.touchEvents.destroy();
3658 this.touchEvents = null;
3659 }
3660 }
3661 },
3662
3663 _setupEventHandlers: function() {
3664 this.eventHandlers.focus = this._onFocus.bind(this);
3665 this.eventHandlers.focusIn = this._onFocusIn.bind(this);
3666 this.eventHandlers.focusOut = this._onFocusOut.bind(this);
3667 this.eventHandlers.blur = this._onBlur.bind(this);
3668 this.eventHandlers.keyUp = this._onKeyUp.bind(this);
3669 this.eventHandlers.clickButton = this._onClickButton.bind(this);
3670 this.eventHandlers.onClickIndicator = this._onClickIndicator.bind(this);
3671 this.eventHandlers.onKeydownIndicator = this._onKeydownIndicator.bind(
3672 this
3673 );
3674 this.eventHandlers.onClickPause = this._onClickPause.bind(this);
3675
3676 if (this.adaptHeight) {
3677 this.eventHandlers.debounceResize = theme.Helpers.debounce(
3678 function() {
3679 this.setSlideshowHeight();
3680 }.bind(this),
3681 50
3682 );
3683
3684 window.addEventListener('resize', this.eventHandlers.debounceResize);
3685 }
3686
3687 this.container.addEventListener('focus', this.eventHandlers.focus, true);
3688 this.slider.addEventListener('focusin', this.eventHandlers.focusIn, true);
3689 this.slider.addEventListener(
3690 'focusout',
3691 this.eventHandlers.focusOut,
3692 true
3693 );
3694 this.container.addEventListener('blur', this.eventHandlers.blur, true);
3695
3696 if (this.buttons) {
3697 this.buttons.forEach(
3698 function(button) {
3699 button.addEventListener('click', this.eventHandlers.clickButton);
3700 }.bind(this)
3701 );
3702 }
3703
3704 if (this.pause) {
3705 this.pause.addEventListener('click', this.eventHandlers.onClickPause);
3706 }
3707
3708 this.indicators.forEach(function(indicatorWrapper) {
3709 indicatorWrapper.childNodes.forEach(function(indicator) {
3710 indicator.firstElementChild.addEventListener(
3711 'click',
3712 this.eventHandlers.onClickIndicator
3713 );
3714
3715 indicator.firstElementChild.addEventListener(
3716 'keydown',
3717 this.eventHandlers.onKeydownIndicator
3718 );
3719 }, this);
3720 }, this);
3721
3722 if (this.options.type === 'slide') {
3723 this.eventHandlers.debounceResizeSlideIn = theme.Helpers.debounce(
3724 function() {
3725 this.sliderItemWidthTotal = 0;
3726 this._setupSlideType(true);
3727 }.bind(this),
3728 50
3729 );
3730
3731 window.addEventListener(
3732 'resize',
3733 this.eventHandlers.debounceResizeSlideIn
3734 );
3735
3736 if (
3737 this.options.canUseTouchEvents &&
3738 this.options.slidesToScroll < this.slides.length
3739 ) {
3740 this._setupTouchEvents();
3741 }
3742 }
3743 },
3744
3745 _setupTouchEvents: function() {
3746 this.touchEvents = new theme.TouchEvents(this.sliderTrack, {
3747 start: function() {
3748 this._onTouchStart();
3749 }.bind(this),
3750 move: function(event, direction, difference) {
3751 this._onTouchMove(event, direction, difference);
3752 }.bind(this),
3753 end: function(event, direction, difference) {
3754 this._onTouchEnd(event, direction, difference);
3755 }.bind(this)
3756 });
3757 },
3758
3759 /**
3760 * Set slideshop for "slide-in" effect
3761 * @param {Boolean} onResize if function call came from resize event
3762 */
3763 _setupSlideType: function(onResize) {
3764 this.sliderItemWidth = Math.floor(
3765 this.sliderContainer.offsetWidth / this.options.slidesToShow
3766 );
3767 this.sliderTranslateXMove =
3768 this.sliderItemWidth * this.options.slidesToScroll;
3769
3770 if (!onResize) {
3771 this.sliderContainer.classList.add(classes.sliderInitialized);
3772 }
3773
3774 // Loop through all slider items
3775 // Set width according to the number of items to show in 1 slide
3776 // Set container width to accomodate all items
3777 this.slides.forEach(function(sliderItem, index) {
3778 var sliderItemLink = sliderItem.querySelector(selectors.sliderItemLink);
3779 sliderItem.style.width = this.sliderItemWidth + 'px';
3780 sliderItem.setAttribute('aria-hidden', true);
3781 sliderItem.setAttribute('tabindex', -1);
3782 this.sliderItemWidthTotal =
3783 this.sliderItemWidthTotal + sliderItem.offsetWidth;
3784
3785 if (sliderItemLink) {
3786 sliderItemLink.setAttribute('tabindex', -1);
3787 }
3788
3789 if (index < this.options.slidesToShow) {
3790 sliderItem.setAttribute('aria-hidden', false);
3791 sliderItem.classList.add(this.options.slideActiveClass);
3792
3793 if (sliderItemLink) {
3794 sliderItemLink.setAttribute('tabindex', 0);
3795 }
3796 }
3797 }, this);
3798
3799 this.sliderTrack.style.width =
3800 Math.floor(this.sliderItemWidthTotal) + 'px';
3801 this.sliderTrack.style.transform = 'translateX(-0px)';
3802
3803 // set disabled attribute on Previous button
3804 if (this.buttons.length) {
3805 this.buttons[0].setAttribute('aria-disabled', true);
3806 this.buttons[1].removeAttribute('aria-disabled');
3807 }
3808
3809 if (this.indicators.length) {
3810 this._setActiveIndicator(0);
3811 }
3812 },
3813
3814 _onTouchStart: function() {
3815 this.touchStartPosition = this._getTranslateXPosition();
3816 },
3817
3818 _onTouchMove: function(event, direction, difference) {
3819 // Fix touch events cause unexpected behaviour
3820 // when the dragging motion goes beyond the theme editor preview.
3821 var threshold = 80;
3822 if (
3823 Shopify.designMode &&
3824 (event.clientX <= threshold ||
3825 event.clientX >= window.innerWidth - threshold)
3826 ) {
3827 event.target.dispatchEvent(
3828 new MouseEvent('mouseup', {
3829 bubbles: true,
3830 cancelable: true
3831 })
3832 );
3833 return;
3834 }
3835
3836 if (direction !== 'left' && direction !== 'right') return;
3837
3838 this.touchMovePosition = this.touchStartPosition + difference.xPosition;
3839
3840 this.sliderTrack.style.transform =
3841 'translateX(' + this.touchMovePosition + 'px';
3842 },
3843
3844 _onTouchEnd: function(event, direction, difference) {
3845 var nextTranslateXPosition = 0;
3846
3847 if (Object.keys(difference).length === 0) return;
3848
3849 var slideDirection = direction === 'left' ? 'next' : '';
3850
3851 if (direction === 'left') {
3852 if (this._isNextTranslateXLast(this.touchStartPosition)) {
3853 nextTranslateXPosition = this.touchStartPosition;
3854 } else {
3855 nextTranslateXPosition =
3856 this.touchStartPosition - this.sliderTranslateXMove;
3857 }
3858 } else {
3859 nextTranslateXPosition =
3860 this.touchStartPosition + this.sliderTranslateXMove;
3861 if (this._isNextTranslateXFirst(this.touchStartPosition)) {
3862 nextTranslateXPosition = 0;
3863 }
3864 }
3865
3866 this.slideIndex = this._getNextSlideIndex(slideDirection);
3867
3868 this.sliderTrack.style.transition = 'transform 500ms ease 0s';
3869 this.sliderTrack.style.transform =
3870 'translateX(' + nextTranslateXPosition + 'px';
3871
3872 window.setTimeout(
3873 function() {
3874 this.sliderTrack.style.transition = '';
3875 }.bind(this),
3876 500
3877 );
3878
3879 this._verifyFirstLastSlideTranslateX(nextTranslateXPosition);
3880
3881 this._postTransitionEnd();
3882 },
3883
3884 /**
3885 * Events handlers for next and previous button
3886 * @param {Object} event event handler
3887 */
3888 _onClickButton: function(event) {
3889 // prevent multiple clicks
3890 if (event.detail > 1) return;
3891
3892 var button = event.currentTarget;
3893 var nextButton = button.hasAttribute(attributes.buttonNext);
3894
3895 if (
3896 this.options.type === 'slide' &&
3897 button.getAttribute('aria-disabled') === 'true'
3898 ) {
3899 return;
3900 }
3901
3902 if (this.options.autoplay && this.isAutoPlaying) {
3903 this.stopAutoplay();
3904 }
3905
3906 if (nextButton) {
3907 this.nextSlide();
3908 } else {
3909 this.previousSlide();
3910 }
3911 },
3912
3913 _onClickIndicator: function(event) {
3914 event.preventDefault();
3915
3916 if (event.target.classList.contains(classes.indicatorActive)) return;
3917
3918 if (this.options.autoplay && this.isAutoPlaying) {
3919 this.stopAutoplay();
3920 }
3921
3922 this.slideIndex = Number(event.target.dataset.slideNumber);
3923 this.goToSlideByIndex(this.slideIndex);
3924 },
3925
3926 goToSlideByIndex: function(index) {
3927 this._setPosition(index);
3928
3929 if (this.options.type === 'slide' && this.sliderTrack) {
3930 this.sliderTrack.style.transition = 'transform 500ms ease 0s';
3931 var newPosition = index * this.slides[0].offsetWidth;
3932
3933 this.sliderTrack.style.transform = 'translateX(-' + newPosition + 'px)';
3934
3935 if (this.options.slidesToShow > 1) {
3936 this._verifyFirstLastSlideTranslateX(newPosition);
3937
3938 if (this.buttons.length) {
3939 this._disableArrows();
3940 }
3941
3942 this._setupMultipleActiveSlide(
3943 index,
3944 index + (this.options.slidesToShow - 1)
3945 );
3946 }
3947 }
3948 },
3949
3950 _onKeydownIndicator: function(event) {
3951 if (event.keyCode !== slate.utils.keyboardKeys.ENTER) return;
3952
3953 this._onClickIndicator(event);
3954
3955 this.slider.focus();
3956 },
3957
3958 _onClickPause: function(event) {
3959 if (!event.currentTarget.classList.contains(classes.isPaused)) {
3960 event.currentTarget.classList.add(classes.isPaused);
3961 this.stopAutoplay();
3962 } else {
3963 event.currentTarget.classList.remove(classes.isPaused);
3964 this.startAutoplay();
3965 }
3966 },
3967
3968 _onFocus: function() {
3969 this.container.addEventListener('keyup', this.eventHandlers.keyUp);
3970 },
3971
3972 _onFocusIn: function() {
3973 if (this.slider.hasAttribute('aria-live')) return;
3974
3975 if (this.options.autoplay && this.isAutoPlaying) {
3976 this.stopAutoplay();
3977 }
3978
3979 this.slider.setAttribute('aria-live', 'polite');
3980 },
3981
3982 _onBlur: function() {
3983 this.container.removeEventListener('keyup', this.eventHandlers.keyUp);
3984 },
3985
3986 _onFocusOut: function() {
3987 this.slider.removeAttribute('aria-live');
3988
3989 // Adding a setTimeout because everytime we focus out
3990 // It automatically goes to <body>
3991 // We want to resume autoplay when focus is outside of the slideshow container
3992 setTimeout(
3993 function() {
3994 if (
3995 !document.activeElement.closest(
3996 '#' + this.slider.getAttribute('id')
3997 )
3998 ) {
3999 if (
4000 this.options.autoplay &&
4001 !this.isAutoPlaying &&
4002 !this.pause.classList.contains(classes.isPaused)
4003 ) {
4004 this.startAutoplay();
4005 }
4006 }
4007 }.bind(this),
4008 this.timeout
4009 );
4010 },
4011
4012 _onKeyUp: function(event) {
4013 switch (event.keyCode) {
4014 case slate.utils.keyboardKeys.LEFTARROW:
4015 if (!this.options.canUseKeyboardArrows) return;
4016
4017 if (this.options.type === 'slide' && this.isFirstSlide) {
4018 return;
4019 }
4020
4021 this.previousSlide();
4022
4023 break;
4024 case slate.utils.keyboardKeys.RIGHTARROW:
4025 if (!this.options.canUseKeyboardArrows) return;
4026
4027 if (this.options.type === 'slide' && this.isLastSlide) {
4028 return;
4029 }
4030
4031 this.nextSlide();
4032
4033 break;
4034 case slate.utils.keyboardKeys.ESCAPE:
4035 this.slider.blur();
4036 break;
4037 }
4038 },
4039
4040 _move: function(direction) {
4041 if (this.options.type === 'slide') {
4042 this.slideIndex = this._getNextSlideIndex(direction);
4043 this._moveSlideshow(direction);
4044 } else {
4045 var nextSlideIndex = this._getNextSlideIndex(direction);
4046 this._setPosition(nextSlideIndex);
4047 }
4048 },
4049
4050 _moveSlideshow: function(direction) {
4051 this.direction = direction;
4052 var valueXToMove = 0;
4053
4054 // Get current position of translateX
4055 var currentTranslateXPosition = this._getTranslateXPosition();
4056 var currentActiveSlidesIndex = this._getActiveSlidesIndex();
4057
4058 // In the future, we'll use ES6 deconstructure
4059 // Math.min(...currentActiveSlidesIndex);
4060 var currentActiveSlidesMinIndex = Math.min.apply(
4061 Math,
4062 currentActiveSlidesIndex
4063 );
4064 var currentActiveSlidesMaxIndex = Math.max.apply(
4065 Math,
4066 currentActiveSlidesIndex
4067 );
4068
4069 // Set the next active state depending on the direction
4070 // We bump up the index depending on the "slidesToShow" option
4071 this.nextMinIndex =
4072 direction === 'next'
4073 ? currentActiveSlidesMinIndex + this.options.slidesToShow
4074 : currentActiveSlidesMinIndex - this.options.slidesToShow;
4075 this.nextMaxIndex =
4076 direction === 'next'
4077 ? currentActiveSlidesMaxIndex + this.options.slidesToShow
4078 : currentActiveSlidesMinIndex - 1;
4079
4080 this.sliderTrack.style.transition = 'transform 500ms ease 0s';
4081
4082 if (direction === 'next') {
4083 valueXToMove = currentTranslateXPosition - this.sliderTranslateXMove;
4084 this.sliderTrack.style.transform = 'translateX(' + valueXToMove + 'px)';
4085 } else {
4086 valueXToMove = currentTranslateXPosition + this.sliderTranslateXMove;
4087 this.sliderTrack.style.transform = 'translateX(' + valueXToMove + 'px)';
4088 }
4089
4090 this._verifyFirstLastSlideTranslateX(valueXToMove);
4091
4092 this._postTransitionEnd();
4093
4094 this._setupMultipleActiveSlide(this.nextMinIndex, this.nextMaxIndex);
4095 },
4096
4097 _setPosition: function(nextSlideIndex) {
4098 this.slideIndex = nextSlideIndex;
4099
4100 if (this.indicators.length) {
4101 this._setActiveIndicator(nextSlideIndex);
4102 }
4103
4104 this._setupActiveSlide(nextSlideIndex);
4105
4106 if (this.options.autoplay && this.isAutoPlaying) {
4107 this.startAutoplay();
4108 }
4109
4110 this.container.dispatchEvent(
4111 new CustomEvent('slider_slide_changed', {
4112 detail: nextSlideIndex
4113 })
4114 );
4115 },
4116
4117 _setupActiveSlide: function(index) {
4118 this.slides.forEach(function(slide) {
4119 slide.setAttribute('aria-hidden', true);
4120 slide.classList.remove(this.options.slideActiveClass);
4121 }, this);
4122
4123 this.slides[index].setAttribute('aria-hidden', false);
4124 this.slides[index].classList.add(this.options.slideActiveClass);
4125 },
4126
4127 /**
4128 * Loops through all slide items
4129 * Set the active state depending the direction and slide indexes
4130 * Because slide-in effect can have multiple items in 1 slide, we need to target multiple active elements
4131 * @param {String} direction "next" for next slides or empty string for previous
4132 * @param {*} minIndex the current active minimum index
4133 * @param {*} maxIndex the current active maximum index
4134 */
4135 _setupMultipleActiveSlide: function(minIndex, maxIndex) {
4136 this.slides.forEach(function(slide) {
4137 var sliderIndex = Number(slide.getAttribute('data-slider-slide-index'));
4138 var sliderItemLink = slide.querySelector(selectors.sliderItemLink);
4139
4140 slide.setAttribute('aria-hidden', true);
4141 slide.classList.remove(this.options.slideActiveClass);
4142 if (sliderItemLink) {
4143 sliderItemLink.setAttribute('tabindex', -1);
4144 }
4145
4146 if (sliderIndex >= minIndex && sliderIndex <= maxIndex) {
4147 slide.setAttribute('aria-hidden', false);
4148 slide.classList.add(this.options.slideActiveClass);
4149
4150 if (sliderItemLink) {
4151 sliderItemLink.setAttribute('tabindex', 0);
4152 }
4153 }
4154 }, this);
4155 },
4156
4157 _setActiveIndicator: function(index) {
4158 this.indicators.forEach(function(indicatorWrapper) {
4159 var activeIndicator = indicatorWrapper.querySelector(
4160 '.' + classes.indicatorActive
4161 );
4162
4163 var nextIndicator = indicatorWrapper.childNodes[index];
4164
4165 if (activeIndicator) {
4166 activeIndicator.setAttribute('aria-selected', false);
4167 activeIndicator.classList.remove(classes.indicatorActive);
4168 activeIndicator.firstElementChild.removeAttribute('aria-current');
4169 }
4170
4171 nextIndicator.classList.add(classes.indicatorActive);
4172 nextIndicator.setAttribute('aria-selected', true);
4173 nextIndicator.firstElementChild.setAttribute('aria-current', true);
4174 }, this);
4175 },
4176
4177 setSlideshowHeight: function() {
4178 var minAspectRatio = this.sliderContainer.getAttribute(
4179 'data-min-aspect-ratio'
4180 );
4181 this.sliderContainer.style.height =
4182 document.documentElement.offsetWidth / minAspectRatio + 'px';
4183 },
4184
4185 /**
4186 * Increase or decrease index position of the slideshow
4187 * Automatically auto-rotate
4188 * - Last slide goes to first slide when clicking "next"
4189 * - First slide goes to last slide when clicking "previous"
4190 * @param {String} direction "next" as a String, other empty string is previous slide
4191 */
4192 _getNextSlideIndex: function(direction) {
4193 var counter = direction === 'next' ? 1 : -1;
4194
4195 if (direction === 'next') {
4196 if (this.slideIndex === this.lastSlide) {
4197 return this.options.type === 'slide' ? this.lastSlide : 0;
4198 }
4199 } else if (!this.slideIndex) {
4200 return this.options.type === 'slide' ? 0 : this.lastSlide;
4201 }
4202
4203 return this.slideIndex + counter;
4204 },
4205
4206 /**
4207 * In "slide-in" type, multiple items are active in 1 slide
4208 * This will return an array containing their indexes
4209 */
4210 _getActiveSlidesIndex: function() {
4211 var currentActiveSlides = this.slides.filter(function(sliderItem) {
4212 if (sliderItem.classList.contains(this.options.slideActiveClass)) {
4213 return sliderItem;
4214 }
4215 }, this);
4216 var currentActiveSlidesIndex = currentActiveSlides.map(function(
4217 sliderItem
4218 ) {
4219 return Number(sliderItem.getAttribute('data-slider-slide-index'));
4220 });
4221
4222 return currentActiveSlidesIndex;
4223 },
4224
4225 /**
4226 * This checks the next "translateX" value and verifies
4227 * If it's at the last slide or beginning of the slide
4228 * So we can disable the arrow buttons
4229 */
4230 _disableArrows: function() {
4231 if (this.buttons.length === 0) return;
4232
4233 var previousButton = this.buttons[0];
4234 var nextButton = this.buttons[1];
4235
4236 // first slide
4237 if (this.isFirstSlide) {
4238 previousButton.setAttribute('aria-disabled', true);
4239 } else {
4240 previousButton.removeAttribute('aria-disabled');
4241 }
4242
4243 // last slide
4244 if (this.isLastSlide) {
4245 nextButton.setAttribute('aria-disabled', true);
4246 } else {
4247 nextButton.removeAttribute('aria-disabled');
4248 }
4249 },
4250
4251 /**
4252 * Verify if translateX reaches at first or last slide
4253 * @param {Number} translateXValue
4254 */
4255 _verifyFirstLastSlideTranslateX: function(translateXValue) {
4256 // first slide
4257 if (this._isNextTranslateXFirst(translateXValue)) {
4258 this.isFirstSlide = true;
4259 } else {
4260 this.isFirstSlide = false;
4261 }
4262
4263 // last slide
4264 if (this._isNextTranslateXLast(translateXValue)) {
4265 this.isLastSlide = true;
4266 } else {
4267 this.isLastSlide = false;
4268 }
4269 },
4270
4271 _getTranslateXPosition: function() {
4272 return Number(this.sliderTrack.style.transform.match(/(-?[0-9]+)/g)[0]);
4273 },
4274
4275 _isNextTranslateXFirst: function(translateXValue) {
4276 return translateXValue === 0;
4277 },
4278
4279 _isNextTranslateXLast: function(translateXValue) {
4280 // because translateX values are using negative, I'm converting into positive value
4281 var translateXValueAbsolute = Math.abs(translateXValue);
4282 var nextTranslateXValue =
4283 translateXValueAbsolute + this.sliderTranslateXMove;
4284
4285 return nextTranslateXValue >= this.sliderItemWidthTotal;
4286 },
4287
4288 _postTransitionEnd: function() {
4289 if (this.buttons.length) {
4290 this._disableArrows();
4291 }
4292
4293 if (this.indicators.length) {
4294 this._setActiveIndicator(this.slideIndex);
4295 }
4296 }
4297 });
4298
4299 return Slideshow;
4300})();
4301
4302theme.Video = (function() {
4303 var autoplayCheckComplete = false;
4304 var playOnClickChecked = false;
4305 var playOnClick = false;
4306 var youtubeLoaded = false;
4307 var videos = {};
4308 var videoPlayers = [];
4309 var videoOptions = {
4310 ratio: 16 / 9,
4311 scrollAnimationDuration: 400,
4312 playerVars: {
4313 // eslint-disable-next-line camelcase
4314 iv_load_policy: 3,
4315 modestbranding: 1,
4316 autoplay: 0,
4317 controls: 0,
4318 wmode: 'opaque',
4319 branding: 0,
4320 autohide: 0,
4321 rel: 0
4322 },
4323 events: {
4324 onReady: onPlayerReady,
4325 onStateChange: onPlayerChange
4326 }
4327 };
4328 var classes = {
4329 playing: 'video-is-playing',
4330 paused: 'video-is-paused',
4331 loading: 'video-is-loading',
4332 loaded: 'video-is-loaded',
4333 backgroundVideoWrapper: 'video-background-wrapper',
4334 videoWithImage: 'video--image_with_play',
4335 backgroundVideo: 'video--background',
4336 userPaused: 'is-paused',
4337 supportsAutoplay: 'autoplay',
4338 supportsNoAutoplay: 'no-autoplay',
4339 wrapperMinHeight: 'video-section-wrapper--min-height'
4340 };
4341
4342 var selectors = {
4343 section: '.video-section',
4344 videoWrapper: '.video-section-wrapper',
4345 playVideoBtn: '.video-control__play',
4346 closeVideoBtn: '.video-control__close-wrapper',
4347 pauseVideoBtn: '.video__pause',
4348 pauseVideoStop: '.video__pause-stop',
4349 pauseVideoResume: '.video__pause-resume',
4350 fallbackText: '.icon__fallback-text'
4351 };
4352
4353 /**
4354 * Public functions
4355 */
4356 function init(video) {
4357 if (!video) return;
4358
4359 videos[video.id] = {
4360 id: video.id,
4361 videoId: video.dataset.id,
4362 type: video.dataset.type,
4363 status:
4364 video.dataset.type === 'image_with_play' ? 'closed' : 'background', // closed, open, background
4365 video: video,
4366 videoWrapper: video.closest(selectors.videoWrapper),
4367 section: video.closest(selectors.section),
4368 controls: video.dataset.type === 'background' ? 0 : 1
4369 };
4370
4371 if (!youtubeLoaded) {
4372 // This code loads the IFrame Player API code asynchronously.
4373 var tag = document.createElement('script');
4374 tag.src = 'https://www.youtube.com/iframe_api';
4375 var firstScriptTag = document.getElementsByTagName('script')[0];
4376 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
4377 }
4378
4379 playOnClickCheck();
4380 }
4381
4382 function customPlayVideo(playerId) {
4383 // Make sure we have carried out the playOnClick check first
4384 if (!playOnClickChecked && !playOnClick) {
4385 return;
4386 }
4387
4388 if (playerId && typeof videoPlayers[playerId].playVideo === 'function') {
4389 privatePlayVideo(playerId);
4390 }
4391 }
4392
4393 function pauseVideo(playerId) {
4394 if (
4395 videoPlayers[playerId] &&
4396 typeof videoPlayers[playerId].pauseVideo === 'function'
4397 ) {
4398 videoPlayers[playerId].pauseVideo();
4399 }
4400 }
4401
4402 function loadVideos() {
4403 for (var key in videos) {
4404 if (videos.hasOwnProperty(key)) {
4405 createPlayer(key);
4406 }
4407 }
4408
4409 initEvents();
4410 youtubeLoaded = true;
4411 }
4412
4413 function editorLoadVideo(key) {
4414 if (!youtubeLoaded) {
4415 return;
4416 }
4417 createPlayer(key);
4418
4419 initEvents();
4420 }
4421
4422 /**
4423 * Private functions
4424 */
4425
4426 function privatePlayVideo(id, clicked) {
4427 var videoData = videos[id];
4428 var player = videoPlayers[id];
4429 var videoWrapper = videoData.videoWrapper;
4430
4431 if (playOnClick) {
4432 // playOnClick means we are probably on mobile (no autoplay).
4433 // setAsPlaying will show the iframe, requiring another click
4434 // to play the video.
4435 setAsPlaying(videoData);
4436 } else if (clicked || autoplayCheckComplete) {
4437 // Play if autoplay is available or clicked to play
4438 videoWrapper.classList.remove(classes.loading);
4439 setAsPlaying(videoData);
4440 player.playVideo();
4441 return;
4442 } else {
4443 player.playVideo();
4444 }
4445 }
4446
4447 function setAutoplaySupport(supported) {
4448 var supportClass = supported
4449 ? classes.supportsAutoplay
4450 : classes.supportsNoAutoplay;
4451 document.documentElement.classList.remove(
4452 classes.supportsAutoplay,
4453 classes.supportsNoAutoplay
4454 );
4455 document.documentElement.classList.add(supportClass);
4456
4457 if (!supported) {
4458 playOnClick = true;
4459 }
4460
4461 autoplayCheckComplete = true;
4462 }
4463
4464 function playOnClickCheck() {
4465 if (playOnClickChecked) {
4466 return;
4467 }
4468
4469 if (isMobile()) {
4470 playOnClick = true;
4471 }
4472
4473 if (playOnClick) {
4474 // No need to also do the autoplay check
4475 setAutoplaySupport(false);
4476 }
4477
4478 playOnClickChecked = true;
4479 }
4480
4481 // The API will call this function when each video player is ready
4482 function onPlayerReady(evt) {
4483 evt.target.setPlaybackQuality('hd1080');
4484 var videoData = getVideoOptions(evt);
4485 var videoTitle = evt.target.getVideoData().title;
4486 playOnClickCheck();
4487
4488 // Prevent tabbing through YouTube player controls until visible
4489 document.getElementById(videoData.id).setAttribute('tabindex', '-1');
4490
4491 sizeBackgroundVideos();
4492 setButtonLabels(videoData.videoWrapper, videoTitle);
4493
4494 // Customize based on options from the video ID
4495 if (videoData.type === 'background') {
4496 evt.target.mute();
4497 privatePlayVideo(videoData.id);
4498 }
4499
4500 videoData.videoWrapper.classList.add(classes.loaded);
4501 }
4502
4503 function onPlayerChange(evt) {
4504 var videoData = getVideoOptions(evt);
4505 if (
4506 videoData.status === 'background' &&
4507 !isMobile() &&
4508 !autoplayCheckComplete &&
4509 (evt.data === YT.PlayerState.PLAYING ||
4510 evt.data === YT.PlayerState.BUFFERING)
4511 ) {
4512 setAutoplaySupport(true);
4513 autoplayCheckComplete = true;
4514 videoData.videoWrapper.classList.remove(classes.loading);
4515 }
4516 switch (evt.data) {
4517 case YT.PlayerState.ENDED:
4518 setAsFinished(videoData);
4519 break;
4520 case YT.PlayerState.PAUSED:
4521 // Seeking on a YouTube video also fires a PAUSED state change,
4522 // checking the state after a delay prevents us pausing the video when
4523 // the user is seeking instead of pausing
4524 setTimeout(function() {
4525 if (evt.target.getPlayerState() === YT.PlayerState.PAUSED) {
4526 setAsPaused(videoData);
4527 }
4528 }, 200);
4529 break;
4530 }
4531 }
4532
4533 function setAsFinished(videoData) {
4534 switch (videoData.type) {
4535 case 'background':
4536 videoPlayers[videoData.id].seekTo(0);
4537 break;
4538 case 'image_with_play':
4539 closeVideo(videoData.id);
4540 toggleExpandVideo(videoData.id, false);
4541 break;
4542 }
4543 }
4544
4545 function setAsPlaying(videoData) {
4546 var videoWrapper = videoData.videoWrapper;
4547 var pauseButton = videoWrapper.querySelector(selectors.pauseVideoBtn);
4548
4549 videoWrapper.classList.remove(classes.loading);
4550
4551 if (pauseButton.classList.contains(classes.userPaused)) {
4552 pauseButton.classList.remove(classes.userPaused);
4553 }
4554
4555 // Do not change element visibility if it is a background video
4556 if (videoData.status === 'background') {
4557 return;
4558 }
4559
4560 document.getElementById(videoData.id).setAttribute('tabindex', '0');
4561
4562 if (videoData.type === 'image_with_play') {
4563 videoWrapper.classList.remove(classes.paused);
4564 videoWrapper.classList.add(classes.playing);
4565 }
4566
4567 // Update focus to the close button so we stay within the video wrapper,
4568 // allowing time for the scroll animation
4569 setTimeout(function() {
4570 videoWrapper.querySelector(selectors.closeVideoBtn).focus();
4571 }, videoOptions.scrollAnimationDuration);
4572 }
4573
4574 function setAsPaused(videoData) {
4575 var videoWrapper = videoData.videoWrapper;
4576
4577 // YT's events fire after our click event. This status flag ensures
4578 // we don't interact with a closed or background video.
4579 if (videoData.type === 'image_with_play') {
4580 if (videoData.status === 'closed') {
4581 videoWrapper.classList.remove(classes.paused);
4582 } else {
4583 videoWrapper.classList.add(classes.paused);
4584 }
4585 }
4586
4587 videoWrapper.classList.remove(classes.playing);
4588 }
4589
4590 function closeVideo(playerId) {
4591 var videoData = videos[playerId];
4592 var videoWrapper = videoData.videoWrapper;
4593
4594 document.getElementById(videoData.id).setAttribute('tabindex', '-1');
4595
4596 videoData.status = 'closed';
4597
4598 switch (videoData.type) {
4599 case 'image_with_play':
4600 videoPlayers[playerId].stopVideo();
4601 setAsPaused(videoData); // in case the video is already paused
4602 break;
4603 case 'background':
4604 videoPlayers[playerId].mute();
4605 setBackgroundVideo(playerId);
4606 break;
4607 }
4608
4609 videoWrapper.classList.remove(classes.paused, classes.playing);
4610 }
4611
4612 function getVideoOptions(evt) {
4613 var id = evt.target.getIframe().id;
4614 return videos[id];
4615 }
4616
4617 function toggleExpandVideo(playerId, expand) {
4618 var video = videos[playerId];
4619 var elementTop =
4620 video.videoWrapper.getBoundingClientRect().top + window.pageYOffset;
4621 var playButton = video.videoWrapper.querySelector(selectors.playVideoBtn);
4622 var offset = 0;
4623 var newHeight = 0;
4624
4625 if (isMobile()) {
4626 video.videoWrapper.parentElement.classList.toggle('page-width', !expand);
4627 }
4628
4629 if (expand) {
4630 if (isMobile()) {
4631 newHeight = window.innerWidth / videoOptions.ratio;
4632 } else {
4633 newHeight = video.videoWrapper.offsetWidth / videoOptions.ratio;
4634 }
4635 offset = (window.innerHeight - newHeight) / 2;
4636
4637 video.videoWrapper.style.height =
4638 video.videoWrapper.getBoundingClientRect().height + 'px';
4639 video.videoWrapper.classList.remove(classes.wrapperMinHeight);
4640 video.videoWrapper.style.height = newHeight + 'px';
4641
4642 // Animate doesn't work in mobile editor, so we don't use it
4643 if (!(isMobile() && Shopify.designMode)) {
4644 var scrollBehavior = document.documentElement.style.scrollBehavior;
4645 document.documentElement.style.scrollBehavior = 'smooth';
4646 window.scrollTo({ top: elementTop - offset });
4647 document.documentElement.style.scrollBehavior = scrollBehavior;
4648 }
4649 } else {
4650 if (isMobile()) {
4651 newHeight = video.videoWrapper.dataset.mobileHeight;
4652 } else {
4653 newHeight = video.videoWrapper.dataset.desktopHeight;
4654 }
4655
4656 video.videoWrapper.style.height = newHeight + 'px';
4657
4658 setTimeout(function() {
4659 video.videoWrapper.classList.add(classes.wrapperMinHeight);
4660 }, 600);
4661 // Set focus on play button, but don't scroll page
4662 var x = window.scrollX;
4663 var y = window.scrollY;
4664 playButton.focus();
4665 window.scrollTo(x, y);
4666 }
4667 }
4668
4669 function togglePause(playerId) {
4670 var pauseButton = videos[playerId].videoWrapper.querySelector(
4671 selectors.pauseVideoBtn
4672 );
4673 var paused = pauseButton.classList.contains(classes.userPaused);
4674 if (paused) {
4675 pauseButton.classList.remove(classes.userPaused);
4676 customPlayVideo(playerId);
4677 } else {
4678 pauseButton.classList.add(classes.userPaused);
4679 pauseVideo(playerId);
4680 }
4681 pauseButton.setAttribute('aria-pressed', !paused);
4682 }
4683
4684 function startVideoOnClick(playerId) {
4685 var video = videos[playerId];
4686
4687 // add loading class to wrapper
4688 video.videoWrapper.classList.add(classes.loading);
4689
4690 // Explicity set the video wrapper height (needed for height transition)
4691 video.videoWrapper.style.height = video.videoWrapper.offsetHeight + 'px';
4692
4693 video.status = 'open';
4694
4695 switch (video.type) {
4696 case 'image_with_play':
4697 privatePlayVideo(playerId, true);
4698 break;
4699 case 'background':
4700 unsetBackgroundVideo(playerId, video);
4701 videoPlayers[playerId].unMute();
4702 privatePlayVideo(playerId, true);
4703 break;
4704 }
4705
4706 toggleExpandVideo(playerId, true);
4707
4708 // esc to close video player
4709 document.addEventListener('keydown', handleVideoPlayerKeydown);
4710 }
4711
4712 var handleVideoPlayerKeydown = function(evt) {
4713 var playerId = document.activeElement.dataset.controls;
4714 if (evt.keyCode !== slate.utils.keyboardKeys.ESCAPE || !playerId) {
4715 return;
4716 }
4717
4718 closeVideo(playerId);
4719 toggleExpandVideo(playerId, false);
4720 };
4721
4722 function sizeBackgroundVideos() {
4723 var backgroundVideos = document.querySelectorAll(
4724 '.' + classes.backgroundVideo
4725 );
4726 backgroundVideos.forEach(function(el) {
4727 sizeBackgroundVideo(el);
4728 });
4729 }
4730
4731 function sizeBackgroundVideo(videoPlayer) {
4732 if (!youtubeLoaded) {
4733 return;
4734 }
4735
4736 if (isMobile()) {
4737 videoPlayer.style.cssText = null;
4738 } else {
4739 var videoWrapper = videoPlayer.closest(selectors.videoWrapper);
4740 var videoWidth = videoWrapper.clientWidth;
4741 var playerWidth = videoPlayer.clientWidth;
4742 var desktopHeight = videoWrapper.dataset.desktopHeight;
4743
4744 // when screen aspect ratio differs from video, video must center and underlay one dimension
4745 if (videoWidth / videoOptions.ratio < desktopHeight) {
4746 playerWidth = Math.ceil(desktopHeight * videoOptions.ratio); // get new player width
4747 var styles =
4748 'width: ' +
4749 playerWidth +
4750 'px; height: ' +
4751 desktopHeight +
4752 'px; left: ' +
4753 (videoWidth - playerWidth) / 2 +
4754 'px; top: 0;';
4755 videoPlayer.style.cssText = styles;
4756 } else {
4757 // new video width < window width (gap to right)
4758 desktopHeight = Math.ceil(videoWidth / videoOptions.ratio); // get new player height
4759 var styles2 =
4760 'width: ' +
4761 videoWidth +
4762 'px; height: ' +
4763 desktopHeight +
4764 'px; top: ' +
4765 (desktopHeight - desktopHeight) / 2 +
4766 'px; left: 0;'; // player height is greater, offset top; reset left
4767 videoPlayer.style.cssText = styles2;
4768 }
4769
4770 theme.Helpers.prepareTransition(videoPlayer);
4771 videoWrapper.classList.add(classes.loaded);
4772 }
4773 }
4774
4775 function unsetBackgroundVideo(playerId) {
4776 // Switch the background video to a chrome-only player once played
4777 var player = document.getElementById(playerId);
4778 player.classList.remove(classes.backgroundVideo);
4779 player.classList.add(classes.videoWithImage);
4780
4781 setTimeout(function() {
4782 document.getElementById(playerId).style.cssText = null;
4783 }, 600);
4784
4785 videos[playerId].videoWrapper.classList.remove(
4786 classes.backgroundVideoWrapper
4787 );
4788 videos[playerId].videoWrapper.classList.add(classes.playing);
4789
4790 videos[playerId].status = 'open';
4791 }
4792
4793 function setBackgroundVideo(playerId) {
4794 var player = document.getElementById(playerId);
4795 player.classList.remove(classes.videoWithImage);
4796 player.classList.add(classes.backgroundVideo);
4797
4798 videos[playerId].videoWrapper.classList.add(classes.backgroundVideoWrapper);
4799
4800 videos[playerId].status = 'background';
4801 sizeBackgroundVideo(player);
4802 }
4803
4804 function isMobile() {
4805 return window.innerWidth < theme.breakpoints.medium;
4806 }
4807
4808 var handleWindowResize = theme.Helpers.debounce(function() {
4809 if (!youtubeLoaded) return;
4810 var key;
4811 var fullscreen = window.innerHeight === screen.height;
4812 sizeBackgroundVideos();
4813
4814 if (isMobile()) {
4815 for (key in videos) {
4816 if (videos.hasOwnProperty(key)) {
4817 if (videos[key].videoWrapper.classList.contains(classes.playing)) {
4818 if (!fullscreen) {
4819 pauseVideo(key);
4820 setAsPaused(videos[key]);
4821 }
4822 }
4823 videos[key].videoWrapper.style.height =
4824 document.documentElement.clientWidth / videoOptions.ratio + 'px';
4825 }
4826 }
4827 setAutoplaySupport(false);
4828 } else {
4829 setAutoplaySupport(true);
4830 for (key in videos) {
4831 var videosWithImage = videos[key].videoWrapper.querySelectorAll(
4832 '.' + classes.videoWithImage
4833 );
4834 if (videosWithImage.length) {
4835 continue;
4836 }
4837 videoPlayers[key].playVideo();
4838 setAsPlaying(videos[key]);
4839 }
4840 }
4841 }, 200);
4842
4843 var handleWindowScroll = theme.Helpers.debounce(function() {
4844 if (!youtubeLoaded) return;
4845
4846 for (var key in videos) {
4847 if (videos.hasOwnProperty(key)) {
4848 var videoWrapper = videos[key].videoWrapper;
4849 var condition =
4850 videoWrapper.getBoundingClientRect().top +
4851 window.pageYOffset +
4852 videoWrapper.offsetHeight * 0.75 <
4853 window.pageYOffset ||
4854 videoWrapper.getBoundingClientRect().top +
4855 window.pageYOffset +
4856 videoWrapper.offsetHeight * 0.25 >
4857 window.pageYOffset + window.innerHeight;
4858
4859 // Close the video if more than 75% of it is scrolled out of view
4860 if (videoWrapper.classList.contains(classes.playing)) {
4861 if (!condition) return;
4862 closeVideo(key);
4863 toggleExpandVideo(key, false);
4864 }
4865 }
4866 }
4867 }, 50);
4868
4869 function initEvents() {
4870 var playVideoBtns = document.querySelectorAll(selectors.playVideoBtn);
4871 var closeVideoBtns = document.querySelectorAll(selectors.closeVideoBtn);
4872 var pauseVideoBtns = document.querySelectorAll(selectors.pauseVideoBtn);
4873
4874 playVideoBtns.forEach(function(btn) {
4875 btn.addEventListener('click', function(evt) {
4876 var playerId = evt.currentTarget.dataset.controls;
4877 startVideoOnClick(playerId);
4878 });
4879 });
4880
4881 closeVideoBtns.forEach(function(btn) {
4882 btn.addEventListener('click', function(evt) {
4883 var playerId = evt.currentTarget.dataset.controls;
4884
4885 evt.currentTarget.blur();
4886 closeVideo(playerId);
4887 toggleExpandVideo(playerId, false);
4888 });
4889 });
4890
4891 pauseVideoBtns.forEach(function(btn) {
4892 btn.addEventListener('click', function(evt) {
4893 var playerId = evt.currentTarget.dataset.controls;
4894 togglePause(playerId);
4895 });
4896 });
4897
4898 // Listen to resize to keep a background-size:cover-like layout
4899 window.addEventListener('resize', handleWindowResize);
4900
4901 window.addEventListener('scroll', handleWindowScroll);
4902 }
4903
4904 function createPlayer(key) {
4905 var args = Object.assign(videoOptions, videos[key]);
4906
4907 args.playerVars.controls = args.controls;
4908 videoPlayers[key] = new YT.Player(key, args);
4909 }
4910
4911 function removeEvents() {
4912 document.removeEventListener('keydown', handleVideoPlayerKeydown);
4913 window.removeEventListener('resize', handleWindowResize);
4914 window.removeEventListener('scroll', handleWindowScroll);
4915 }
4916
4917 function setButtonLabels(videoWrapper, title) {
4918 var playButtons = videoWrapper.querySelectorAll(selectors.playVideoBtn);
4919 var closeButton = videoWrapper.querySelector(selectors.closeVideoBtn);
4920 var pauseButton = videoWrapper.querySelector(selectors.pauseVideoBtn);
4921 var closeButtonText = closeButton.querySelector(selectors.fallbackText);
4922
4923 var pauseButtonStop = pauseButton.querySelector(selectors.pauseVideoStop);
4924 var pauseButtonStopText = pauseButtonStop.querySelector(
4925 selectors.fallbackText
4926 );
4927
4928 var pauseButtonResume = pauseButton.querySelector(
4929 selectors.pauseVideoResume
4930 );
4931 var pauseButtonResumeText = pauseButtonResume.querySelector(
4932 selectors.fallbackText
4933 );
4934
4935 // Insert the video title retrieved from YouTube into the instructional text
4936 // for each button
4937 playButtons.forEach(function(playButton) {
4938 var playButtonText = playButton.querySelector(selectors.fallbackText);
4939
4940 playButtonText.textContent = playButtonText.textContent.replace(
4941 '[video_title]',
4942 title
4943 );
4944 });
4945 closeButtonText.textContent = closeButtonText.textContent.replace(
4946 '[video_title]',
4947 title
4948 );
4949 pauseButtonStopText.textContent = pauseButtonStopText.textContent.replace(
4950 '[video_title]',
4951 title
4952 );
4953 pauseButtonResumeText.textContent = pauseButtonResumeText.textContent.replace(
4954 '[video_title]',
4955 title
4956 );
4957 }
4958
4959 return {
4960 init: init,
4961 editorLoadVideo: editorLoadVideo,
4962 loadVideos: loadVideos,
4963 playVideo: customPlayVideo,
4964 pauseVideo: pauseVideo,
4965 removeEvents: removeEvents
4966 };
4967})();
4968
4969theme.ProductVideo = (function() {
4970 var videos = {};
4971
4972 var hosts = {
4973 html5: 'html5',
4974 youtube: 'youtube'
4975 };
4976
4977 var selectors = {
4978 productMediaWrapper: '[data-product-single-media-wrapper]'
4979 };
4980
4981 var attributes = {
4982 enableVideoLooping: 'enable-video-looping',
4983 videoId: 'video-id'
4984 };
4985
4986 function init(videoContainer, sectionId) {
4987 if (!videoContainer) {
4988 return;
4989 }
4990
4991 var videoElement = videoContainer.querySelector('iframe, video');
4992
4993 if (!videoElement) {
4994 return;
4995 }
4996
4997 var mediaId = videoContainer.getAttribute('data-media-id');
4998
4999 videos[mediaId] = {
5000 mediaId: mediaId,
5001 sectionId: sectionId,
5002 host: hostFromVideoElement(videoElement),
5003 container: videoContainer,
5004 element: videoElement,
5005 ready: function() {
5006 createPlayer(this);
5007 }
5008 };
5009
5010 var video = videos[mediaId];
5011
5012 switch (video.host) {
5013 case hosts.html5:
5014 window.Shopify.loadFeatures([
5015 {
5016 name: 'video-ui',
5017 version: '1.0',
5018 onLoad: setupPlyrVideos
5019 }
5020 ]);
5021 theme.LibraryLoader.load('plyrShopifyStyles');
5022 break;
5023 case hosts.youtube:
5024 theme.LibraryLoader.load('youtubeSdk', setupYouTubeVideos);
5025 break;
5026 }
5027 }
5028
5029 function setupPlyrVideos(errors) {
5030 if (errors) {
5031 fallbackToNativeVideo();
5032 return;
5033 }
5034
5035 loadVideos(hosts.html5);
5036 }
5037
5038 function setupYouTubeVideos() {
5039 if (!window.YT.Player) return;
5040
5041 loadVideos(hosts.youtube);
5042 }
5043
5044 function createPlayer(video) {
5045 if (video.player) {
5046 return;
5047 }
5048
5049 var productMediaWrapper = video.container.closest(
5050 selectors.productMediaWrapper
5051 );
5052
5053 var enableLooping = productMediaWrapper.getAttribute(
5054 'data-' + attributes.enableVideoLooping
5055 );
5056
5057 switch (video.host) {
5058 case hosts.html5:
5059 // eslint-disable-next-line no-undef
5060 video.player = new Shopify.Plyr(video.element, {
5061 loop: { active: enableLooping }
5062 });
5063 break;
5064 case hosts.youtube:
5065 var videoId = productMediaWrapper.getAttribute(
5066 'data-' + attributes.videoId
5067 );
5068
5069 video.player = new YT.Player(video.element, {
5070 videoId: videoId,
5071 events: {
5072 onStateChange: function(event) {
5073 if (event.data === 0 && enableLooping) event.target.seekTo(0);
5074 }
5075 }
5076 });
5077 break;
5078 }
5079
5080 var pauseVideo = function() {
5081 if (!video.player) return;
5082
5083 if (video.host === hosts.html5) {
5084 video.player.pause();
5085 }
5086
5087 if (video.host === hosts.youtube && video.player.pauseVideo) {
5088 video.player.pauseVideo();
5089 }
5090 };
5091
5092 productMediaWrapper.addEventListener('mediaHidden', pauseVideo);
5093 productMediaWrapper.addEventListener('xrLaunch', pauseVideo);
5094
5095 productMediaWrapper.addEventListener('mediaVisible', function() {
5096 if (theme.Helpers.isTouch()) return;
5097 if (!video.player) return;
5098
5099 if (video.host === hosts.html5) {
5100 video.player.play();
5101 }
5102
5103 if (video.host === hosts.youtube && video.player.playVideo) {
5104 video.player.playVideo();
5105 }
5106 });
5107 }
5108
5109 function hostFromVideoElement(video) {
5110 if (video.tagName === 'VIDEO') {
5111 return hosts.html5;
5112 }
5113
5114 if (video.tagName === 'IFRAME') {
5115 if (
5116 /^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(
5117 video.src
5118 )
5119 ) {
5120 return hosts.youtube;
5121 }
5122 }
5123 return null;
5124 }
5125
5126 function loadVideos(host) {
5127 for (var key in videos) {
5128 if (videos.hasOwnProperty(key)) {
5129 var video = videos[key];
5130 if (video.host === host) {
5131 video.ready();
5132 }
5133 }
5134 }
5135 }
5136
5137 function fallbackToNativeVideo() {
5138 for (var key in videos) {
5139 if (videos.hasOwnProperty(key)) {
5140 var video = videos[key];
5141
5142 if (video.nativeVideo) continue;
5143
5144 if (video.host === hosts.html5) {
5145 video.element.setAttribute('controls', 'controls');
5146 video.nativeVideo = true;
5147 }
5148 }
5149 }
5150 }
5151
5152 function removeSectionVideos(sectionId) {
5153 for (var key in videos) {
5154 if (videos.hasOwnProperty(key)) {
5155 var video = videos[key];
5156
5157 if (video.sectionId === sectionId) {
5158 if (video.player) video.player.destroy();
5159 delete videos[key];
5160 }
5161 }
5162 }
5163 }
5164
5165 return {
5166 init: init,
5167 hosts: hosts,
5168 loadVideos: loadVideos,
5169 removeSectionVideos: removeSectionVideos
5170 };
5171})();
5172
5173theme.ProductModel = (function() {
5174 var modelJsonSections = {};
5175 var models = {};
5176 var xrButtons = {};
5177
5178 var selectors = {
5179 mediaGroup: '[data-product-single-media-group]',
5180 xrButton: '[data-shopify-xr]'
5181 };
5182
5183 function init(modelViewerContainers, sectionId) {
5184 modelJsonSections[sectionId] = {
5185 loaded: false
5186 };
5187
5188 modelViewerContainers.forEach(function(modelViewerContainer, index) {
5189 var mediaId = modelViewerContainer.getAttribute('data-media-id');
5190 var modelViewerElement = modelViewerContainer.querySelector(
5191 'model-viewer'
5192 );
5193 var modelId = modelViewerElement.getAttribute('data-model-id');
5194
5195 if (index === 0) {
5196 var mediaGroup = modelViewerContainer.closest(selectors.mediaGroup);
5197 var xrButton = mediaGroup.querySelector(selectors.xrButton);
5198 xrButtons[sectionId] = {
5199 element: xrButton,
5200 defaultId: modelId
5201 };
5202 }
5203
5204 models[mediaId] = {
5205 modelId: modelId,
5206 sectionId: sectionId,
5207 container: modelViewerContainer,
5208 element: modelViewerElement
5209 };
5210 });
5211
5212 window.Shopify.loadFeatures([
5213 {
5214 name: 'shopify-xr',
5215 version: '1.0',
5216 onLoad: setupShopifyXr
5217 },
5218 {
5219 name: 'model-viewer-ui',
5220 version: '1.0',
5221 onLoad: setupModelViewerUi
5222 }
5223 ]);
5224 theme.LibraryLoader.load('modelViewerUiStyles');
5225 }
5226
5227 function setupShopifyXr(errors) {
5228 if (errors) return;
5229
5230 if (!window.ShopifyXR) {
5231 document.addEventListener('shopify_xr_initialized', function() {
5232 setupShopifyXr();
5233 });
5234 return;
5235 }
5236
5237 for (var sectionId in modelJsonSections) {
5238 if (modelJsonSections.hasOwnProperty(sectionId)) {
5239 var modelSection = modelJsonSections[sectionId];
5240
5241 if (modelSection.loaded) continue;
5242 var modelJson = document.querySelector('#ModelJson-' + sectionId);
5243
5244 window.ShopifyXR.addModels(JSON.parse(modelJson.innerHTML));
5245 modelSection.loaded = true;
5246 }
5247 }
5248 window.ShopifyXR.setupXRElements();
5249 }
5250
5251 function setupModelViewerUi(errors) {
5252 if (errors) return;
5253
5254 for (var key in models) {
5255 if (models.hasOwnProperty(key)) {
5256 var model = models[key];
5257 if (!model.modelViewerUi) {
5258 model.modelViewerUi = new Shopify.ModelViewerUI(model.element);
5259 }
5260 setupModelViewerListeners(model);
5261 }
5262 }
5263 }
5264
5265 function setupModelViewerListeners(model) {
5266 var xrButton = xrButtons[model.sectionId];
5267
5268 model.container.addEventListener('mediaVisible', function() {
5269 xrButton.element.setAttribute('data-shopify-model3d-id', model.modelId);
5270 if (theme.Helpers.isTouch()) return;
5271 model.modelViewerUi.play();
5272 });
5273
5274 model.container.addEventListener('mediaHidden', function() {
5275 xrButton.element.setAttribute(
5276 'data-shopify-model3d-id',
5277 xrButton.defaultId
5278 );
5279 model.modelViewerUi.pause();
5280 });
5281
5282 model.container.addEventListener('xrLaunch', function() {
5283 model.modelViewerUi.pause();
5284 });
5285 }
5286
5287 function removeSectionModels(sectionId) {
5288 for (var key in models) {
5289 if (models.hasOwnProperty(key)) {
5290 var model = models[key];
5291 if (model.sectionId === sectionId) {
5292 models[key].modelViewerUi.destroy();
5293 delete models[key];
5294 }
5295 }
5296 }
5297 delete modelJsonSections[sectionId];
5298 }
5299
5300 return {
5301 init: init,
5302 removeSectionModels: removeSectionModels
5303 };
5304})();
5305
5306window.theme = window.theme || {};
5307
5308theme.FormStatus = (function() {
5309 var selectors = {
5310 statusMessage: '[data-form-status]'
5311 };
5312
5313 function init() {
5314 var statusMessages = document.querySelectorAll(selectors.statusMessage);
5315
5316 statusMessages.forEach(function(statusMessage) {
5317 statusMessage.setAttribute('tabindex', -1);
5318 statusMessage.focus();
5319
5320 statusMessage.addEventListener(
5321 'blur',
5322 function(evt) {
5323 evt.target.removeAttribute('tabindex');
5324 },
5325 { once: true }
5326 );
5327 });
5328 }
5329
5330 return {
5331 init: init
5332 };
5333})();
5334
5335theme.Hero = (function() {
5336 var classes = {
5337 indexSectionFlush: 'index-section--flush'
5338 };
5339
5340 var selectors = {
5341 heroFixedWidthContent: '.hero-fixed-width__content',
5342 heroFixedWidthImage: '.hero-fixed-width__image'
5343 };
5344
5345 function hero(el, sectionId) {
5346 var hero = document.querySelector(el);
5347 var layout = hero.getAttribute('data-layout');
5348 var parentSection = document.querySelector('#shopify-section-' + sectionId);
5349 var heroContent = parentSection.querySelector(
5350 selectors.heroFixedWidthContent
5351 );
5352 var heroImage = parentSection.querySelector(selectors.heroFixedWidthImage);
5353
5354 if (layout !== 'fixed_width') {
5355 return;
5356 }
5357
5358 parentSection.classList.remove(classes.indexSectionFlush);
5359 heroFixedHeight();
5360
5361 window.addEventListener('resize', function() {
5362 theme.Helpers.debounce(function() {
5363 heroFixedHeight();
5364 }, 50);
5365 });
5366
5367 function heroFixedHeight() {
5368 var contentHeight;
5369 var imageHeight;
5370
5371 if (heroContent) {
5372 contentHeight = heroContent.offsetHeight + 50;
5373 }
5374
5375 if (heroImage) {
5376 imageHeight = heroImage.offsetHeight;
5377 }
5378
5379 if (contentHeight > imageHeight) {
5380 heroImage.style.minHeight = contentHeight + 'px';
5381 }
5382 }
5383 }
5384
5385 return hero;
5386})();
5387
5388// prettier-ignore
5389window.theme = window.theme || {};
5390
5391theme.SearchResultsTemplate = (function() {
5392 function renderResults(products, isLoading, searchQuery) {
5393 return [
5394 '<div class="predictive-search">',
5395 renderHeader(products, isLoading),
5396 renderProducts(products, searchQuery),
5397 '</div>'
5398 ].join('');
5399 }
5400
5401 function renderHeader(products, isLoading) {
5402 if (products.length === 0) {
5403 return '';
5404 }
5405
5406 return [
5407 '<div class="predictive-search-title">',
5408 '<h3 id="predictive-search" class="predictive-search-title__content">' +
5409 theme.strings.products +
5410 '</h3>',
5411 '<span class="predictive-search-title__loading-spinner">' +
5412 (isLoading
5413 ? '<span class= "icon-predictive-search-spinner" ></span >'
5414 : '') +
5415 '</span>',
5416 '</div>'
5417 ].join('');
5418 }
5419
5420 function loadingState() {
5421 return [
5422 '<div class="predictive-search">',
5423 '<div class="predictive-search-loading">',
5424 '<span class="visually-hidden">' + theme.strings.loading + '</span>',
5425 '<span class="predictive-search-loading__icon">',
5426 '<span class="icon-predictive-search-spinner"></span>',
5427 '</span>',
5428 '</div>',
5429 '</div>'
5430 ].join('');
5431 }
5432
5433 function renderViewAll(searchQuery) {
5434 return [
5435 '<button type="submit" class="predictive-search-view-all__button" tabindex="-1">',
5436 theme.strings.searchFor +
5437 '<span class="predictive-search-view-all__query"> “' +
5438 _htmlEscape(searchQuery) +
5439 '”</span>',
5440 '</button>'
5441 ].join('');
5442 }
5443
5444 function renderProducts(products, searchQuery) {
5445 var resultsCount = products.length;
5446
5447 return [
5448 '<ul id="predictive-search-results" class="predictive-search__list" role="listbox" aria-labelledby="predictive-search">',
5449 products
5450 .map(function(product, index) {
5451 return renderProduct(normalizeProduct(product), index, resultsCount);
5452 })
5453 .join(''),
5454 '<li id="search-all" class="predictive-search-view-all" role="option" data-search-result>' +
5455 renderViewAll(searchQuery) +
5456 '</li>',
5457 '</ul>'
5458 ].join('');
5459 }
5460
5461 function renderProduct(product, index, resultsCount) {
5462 return [
5463 '<li id="search-result-' +
5464 index +
5465 '" class="predictive-search-item" role="option" data-search-result>',
5466 '<a class="predictive-search-item__link" href="' +
5467 product.url +
5468 '" tabindex="-1">',
5469 '<div class="predictive-search__column predictive-search__column--image" data-image-loading-animation>',
5470 renderProductImage(product),
5471 '</div>',
5472 '<div class="predictive-search__column predictive-search__column--content ' +
5473 (getDetailsCount() ? '' : 'predictive-search__column--center') +
5474 '">',
5475 '<span class="predictive-search-item__title">',
5476 '<span class="predictive-search-item__title-text">' +
5477 product.title +
5478 '</span>',
5479 '</span>' + (getDetailsCount() ? renderProductDetails(product) : ''),
5480 '<span class="visually-hidden">, </span>',
5481 '<span class="visually-hidden">' +
5482 getNumberOfResultsString(index + 1, resultsCount) +
5483 '</span>',
5484 '</div>',
5485 '</a>',
5486 '</li>'
5487 ].join('');
5488 }
5489
5490 function renderProductImage(product) {
5491 if (product.image === null) {
5492 return '';
5493 }
5494
5495 return (
5496 '<img class="predictive-search-item__image lazyload" src="' +
5497 product.image.url +
5498 '" data-src="' +
5499 product.image.url +
5500 '" data-image alt="' +
5501 product.image.alt +
5502 '" />'
5503 );
5504 }
5505
5506 function renderProductDetails(product) {
5507 return [
5508 '<dl class="predictive-search-item__details price' +
5509 (product.isOnSale ? ' price--on-sale' : '') +
5510 (!product.available ? ' price--sold-out' : '') +
5511 (!product.isPriceVaries && product.isCompareVaries
5512 ? ' price--compare-price-hidden'
5513 : '') +
5514 '">',
5515 '<div class="predictive-search-item__detail">',
5516 renderVendor(product),
5517 '</div>',
5518 '<div class="predictive-search-item__detail predictive-search-item__detail--inline">' +
5519 renderProductPrice(product),
5520 '</div>',
5521 '</dl>'
5522 ].join('');
5523 }
5524 function renderProductPrice(product) {
5525 if (!theme.settings.predictiveSearchShowPrice) {
5526 return '';
5527 }
5528
5529 var accessibilityAnnounceComma = '<span class="visually-hidden">, </span>';
5530
5531 var priceMarkup =
5532 '<div class="price__regular">' + renderPrice(product) + '</div>';
5533
5534 var salePriceMarkup =
5535 '<div class="price__sale">' + renderSalePrice(product) + '</div>';
5536
5537 return (
5538 accessibilityAnnounceComma +
5539 '<div class="price__pricing-group">' +
5540 (product.isOnSale ? salePriceMarkup : priceMarkup) +
5541 '</div>'
5542 );
5543 }
5544
5545 function renderSalePrice(product) {
5546 return [
5547 '<dt>',
5548 '<span class="visually-hidden">' + theme.strings.salePrice + '</span>',
5549 '</dt>',
5550 '<dd>',
5551 '<span class="predictive-search-item__price predictive-search-item__price--sale">' +
5552 (product.isPriceVaries
5553 ? theme.strings.fromLowestPrice.replace('[price]', product.price)
5554 : product.price) +
5555 '</span>',
5556 '</dd>',
5557 '<div class="price__compare">' + renderCompareAtPrice(product) + '</div>'
5558 ].join('');
5559 }
5560
5561 function renderCompareAtPrice(product) {
5562 return [
5563 '<dt>',
5564 '<span class="visually-hidden">' +
5565 theme.strings.regularPrice +
5566 '</span> ',
5567 '</dt>',
5568 '<dd>',
5569 '<span class="predictive-search-item__price predictive-search-item__price--compare">' +
5570 product.compareAtPrice +
5571 '</span>',
5572 '</dd>'
5573 ].join('');
5574 }
5575
5576 function renderPrice(product) {
5577 return [
5578 '<dt>',
5579 '<span class="visually-hidden">' + theme.strings.regularPrice + '</span>',
5580 '</dt>',
5581 '<dd>',
5582 '<span class="predictive-search-item__price">' +
5583 (product.isPriceVaries
5584 ? theme.strings.fromLowestPrice.replace('[price]', product.price)
5585 : product.price) +
5586 '</span>',
5587 '</dd>'
5588 ].join('');
5589 }
5590
5591 function renderVendor(product) {
5592 if (!theme.settings.predictiveSearchShowVendor || product.vendor === '') {
5593 return '';
5594 }
5595
5596 return [
5597 '<dt>',
5598 '<span class="visually-hidden">' + theme.strings.vendor + '</span>',
5599 '</dt>',
5600 '<dd class="predictive-search-item__vendor">' + product.vendor + '</dd>'
5601 ].join('');
5602 }
5603
5604 function normalizeProduct(product) {
5605 var productOrVariant =
5606 product.variants.length > 0 ? product.variants[0] : product;
5607
5608 return {
5609 url: productOrVariant.url,
5610 image: getProductImage(product),
5611 title: product.title,
5612 vendor: product.vendor || '',
5613 price: theme.Currency.formatMoney(product.price_min, theme.moneyFormat),
5614 compareAtPrice: theme.Currency.formatMoney(
5615 product.compare_at_price_min,
5616 theme.moneyFormat
5617 ),
5618 available: product.available,
5619 isOnSale: isOnSale(product),
5620 isPriceVaries: isPriceVaries(product),
5621 isCompareVaries: isCompareVaries(product)
5622 };
5623 }
5624
5625 function getProductImage(product) {
5626 var image;
5627 var featuredImage;
5628
5629 if (product.variants.length > 0 && product.variants[0].image !== null) {
5630 featuredImage = product.variants[0].featured_image;
5631 } else if (product.image) {
5632 featuredImage = product.featured_image;
5633 } else {
5634 image = null;
5635 }
5636
5637 if (image !== null) {
5638 image = {
5639 url: theme.Images.getSizedImageUrl(featuredImage.url, '100x'),
5640 alt: featuredImage.alt
5641 };
5642 }
5643
5644 return image;
5645 }
5646
5647 function isOnSale(product) {
5648 return (
5649 product.compare_at_price_min !== null &&
5650 parseInt(product.compare_at_price_min, 10) >
5651 parseInt(product.price_min, 10)
5652 );
5653 }
5654
5655 function isPriceVaries(product) {
5656 return product.price_max !== product.price_min;
5657 }
5658
5659 function isCompareVaries(product) {
5660 return product.compare_at_price_max !== product.compare_at_price_min;
5661 }
5662
5663 // Returns the number of optional product details to be shown,
5664 // values of the detailsList need to be boolean.
5665 function getDetailsCount() {
5666 var detailsList = [
5667 theme.settings.predictiveSearchShowPrice,
5668 theme.settings.predictiveSearchShowVendor
5669 ];
5670
5671 var detailsCount = detailsList.reduce(function(acc, detail) {
5672 return acc + (detail ? 1 : 0);
5673 }, 0);
5674
5675 return detailsCount;
5676 }
5677
5678 function getNumberOfResultsString(resultNumber, resultsCount) {
5679 return theme.strings.number_of_results
5680 .replace('[result_number]', resultNumber)
5681 .replace('[results_count]', resultsCount);
5682 }
5683
5684 function _htmlEscape(input) {
5685 return input
5686 .replace(/&/g, '&')
5687 .replace(/</g, '<')
5688 .replace(/>/g, '>')
5689 .replace(/"/g, '"')
5690 .replace(/'/g, ''');
5691 }
5692
5693 return function(data) {
5694 var products = data.products || [];
5695 var isLoading = data.isLoading;
5696 var searchQuery = data.searchQuery || '';
5697
5698 if (isLoading && products.length === 0) {
5699 return loadingState();
5700 }
5701
5702 return renderResults(products, isLoading, searchQuery);
5703 };
5704})();
5705
5706window.theme = window.theme || {};
5707
5708(function() {
5709 // (a11y) This function will be used by the Predictive Search Component
5710 // to announce the number of search results
5711 function numberOfResultsTemplateFct(data) {
5712 if (data.products.length === 1) {
5713 return theme.strings.one_result_found;
5714 } else {
5715 return theme.strings.number_of_results_found.replace(
5716 '[results_count]',
5717 data.products.length
5718 );
5719 }
5720 }
5721
5722 // (a11y) This function will be used by the Predictive Search Component
5723 // to announce that it's loading results
5724 function loadingResultsMessageTemplateFct() {
5725 return theme.strings.loading;
5726 }
5727
5728 function isPredictiveSearchSupported() {
5729 var shopifyFeatures = JSON.parse(
5730 document.getElementById('shopify-features').textContent
5731 );
5732
5733 return shopifyFeatures.predictiveSearch;
5734 }
5735
5736 function isPredictiveSearchEnabled() {
5737 return window.theme.settings.predictiveSearchEnabled;
5738 }
5739
5740 function canInitializePredictiveSearch() {
5741 return isPredictiveSearchSupported() && isPredictiveSearchEnabled();
5742 }
5743
5744 // listen for search submits and validate query
5745 function validateSearchHandler(searchEl, submitEl) {
5746 submitEl.addEventListener(
5747 'click',
5748 validateSearchInput.bind(this, searchEl)
5749 );
5750 }
5751
5752 // if there is nothing in the search field, prevent submit
5753 function validateSearchInput(searchEl, evt) {
5754 var isInputValueEmpty = searchEl.value.trim().length === 0;
5755 if (!isInputValueEmpty) {
5756 return;
5757 }
5758
5759 if (typeof evt !== 'undefined') {
5760 evt.preventDefault();
5761 }
5762
5763 searchEl.focus();
5764 }
5765
5766 window.theme.SearchPage = (function() {
5767 var selectors = {
5768 searchReset: '[data-search-page-predictive-search-clear]',
5769 searchInput: '[data-search-page-predictive-search-input]',
5770 searchSubmit: '[data-search-page-predictive-search-submit]',
5771 searchResults: '[data-predictive-search-mount="default"]'
5772 };
5773
5774 var componentInstance;
5775
5776 function init(config) {
5777 var searchInput = document.querySelector(selectors.searchInput);
5778 var searchSubmit = document.querySelector(selectors.searchSubmit);
5779 var searchUrl = searchInput.dataset.baseUrl;
5780
5781 componentInstance = new window.Shopify.theme.PredictiveSearchComponent({
5782 selectors: {
5783 input: selectors.searchInput,
5784 reset: selectors.searchReset,
5785 result: selectors.searchResults
5786 },
5787 searchUrl: searchUrl,
5788 resultTemplateFct: window.theme.SearchResultsTemplate,
5789 numberOfResultsTemplateFct: numberOfResultsTemplateFct,
5790 loadingResultsMessageTemplateFct: loadingResultsMessageTemplateFct,
5791 onOpen: function(nodes) {
5792 if (config.isTabletAndUp) {
5793 return;
5794 }
5795
5796 var searchInputBoundingRect = searchInput.getBoundingClientRect();
5797 var bodyHeight = document.body.offsetHeight;
5798 var offset = 50;
5799 var resultsMaxHeight =
5800 bodyHeight - searchInputBoundingRect.bottom - offset;
5801
5802 nodes.result.style.maxHeight = resultsMaxHeight + 'px';
5803 },
5804 onBeforeDestroy: function(nodes) {
5805 // If the viewport width changes from mobile to tablet
5806 // reset the top position of the results
5807 nodes.result.style.maxHeight = '';
5808 }
5809 });
5810
5811 validateSearchHandler(searchInput, searchSubmit);
5812 }
5813
5814 function unload() {
5815 if (!componentInstance) {
5816 return;
5817 }
5818 componentInstance.destroy();
5819 componentInstance = null;
5820 }
5821
5822 return {
5823 init: init,
5824 unload: unload
5825 };
5826 })();
5827
5828 window.theme.SearchHeader = (function() {
5829 var selectors = {
5830 searchInput: '[data-predictive-search-drawer-input]',
5831 searchResults: '[data-predictive-search-mount="drawer"]',
5832 searchFormContainer: '[data-search-form-container]',
5833 searchSubmit: '[data-search-form-submit]'
5834 };
5835
5836 var componentInstance;
5837
5838 function init(config) {
5839 var searchInput = document.querySelector(selectors.searchInput);
5840 var searchSubmit = document.querySelector(selectors.searchSubmit);
5841 var searchUrl = searchInput.dataset.baseUrl;
5842
5843 componentInstance = new window.Shopify.theme.PredictiveSearchComponent({
5844 selectors: {
5845 input: selectors.searchInput,
5846 result: selectors.searchResults
5847 },
5848 searchUrl: searchUrl,
5849 resultTemplateFct: window.theme.SearchResultsTemplate,
5850 numberOfResultsTemplateFct: numberOfResultsTemplateFct,
5851 numberOfResults: config.numberOfResults,
5852 loadingResultsMessageTemplateFct: loadingResultsMessageTemplateFct,
5853 onInputBlur: function() {
5854 return false;
5855 },
5856 onOpen: function(nodes) {
5857 var searchInputBoundingRect = searchInput.getBoundingClientRect();
5858
5859 // For tablet screens and up, stop the scroll area from extending past
5860 // the bottom of the screen because we're locking the body scroll
5861 var maxHeight =
5862 window.innerHeight -
5863 searchInputBoundingRect.bottom -
5864 (config.isTabletAndUp ? 20 : 0);
5865
5866 nodes.result.style.top = config.isTabletAndUp
5867 ? ''
5868 : searchInputBoundingRect.bottom + 'px';
5869 nodes.result.style.maxHeight = maxHeight + 'px';
5870 },
5871 onClose: function(nodes) {
5872 nodes.result.style.maxHeight = '';
5873 },
5874 onBeforeDestroy: function(nodes) {
5875 // If the viewport width changes from mobile to tablet
5876 // reset the top position of the results
5877 nodes.result.style.top = '';
5878 }
5879 });
5880
5881 validateSearchHandler(searchInput, searchSubmit);
5882 }
5883
5884 function unload() {
5885 if (!componentInstance) {
5886 return;
5887 }
5888
5889 componentInstance.destroy();
5890 componentInstance = null;
5891 }
5892
5893 function clearAndClose() {
5894 if (!componentInstance) {
5895 return;
5896 }
5897
5898 componentInstance.clearAndClose();
5899 }
5900
5901 return {
5902 init: init,
5903 unload: unload,
5904 clearAndClose: clearAndClose
5905 };
5906 })();
5907
5908 window.theme.Search = (function() {
5909 var classes = {
5910 searchTemplate: 'template-search'
5911 };
5912 var selectors = {
5913 siteHeader: '.site-header'
5914 };
5915 var mediaQueryList = {
5916 mobile: window.matchMedia('(max-width: 749px)'),
5917 tabletAndUp: window.matchMedia('(min-width: 750px)')
5918 };
5919
5920 function init() {
5921 if (!document.querySelector(selectors.siteHeader)) {
5922 return;
5923 }
5924
5925 if (!canInitializePredictiveSearch()) {
5926 return;
5927 }
5928
5929 Object.keys(mediaQueryList).forEach(function(device) {
5930 mediaQueryList[device].addListener(initSearchAccordingToViewport);
5931 });
5932
5933 initSearchAccordingToViewport();
5934 }
5935
5936 function initSearchAccordingToViewport() {
5937 theme.SearchDrawer.close();
5938 theme.SearchHeader.unload();
5939 theme.SearchPage.unload();
5940
5941 if (mediaQueryList.mobile.matches) {
5942 theme.SearchHeader.init({
5943 numberOfResults: 4,
5944 isTabletAndUp: false
5945 });
5946
5947 if (isSearchPage()) {
5948 theme.SearchPage.init({ isTabletAndUp: false });
5949 }
5950 } else {
5951 // Tablet and up
5952 theme.SearchHeader.init({
5953 numberOfResults: 4,
5954 isTabletAndUp: true
5955 });
5956
5957 if (isSearchPage()) {
5958 theme.SearchPage.init({ isTabletAndUp: true });
5959 }
5960 }
5961 }
5962
5963 function isSearchPage() {
5964 return document.body.classList.contains(classes.searchTemplate);
5965 }
5966
5967 function unload() {
5968 theme.SearchHeader.unload();
5969 theme.SearchPage.unload();
5970 }
5971
5972 return {
5973 init: init,
5974 unload: unload
5975 };
5976 })();
5977})();
5978
5979window.theme = window.theme || {};
5980
5981theme.SearchDrawer = (function() {
5982 var selectors = {
5983 headerSection: '[data-header-section]',
5984 drawer: '[data-predictive-search-drawer]',
5985 drawerOpenButton: '[data-predictive-search-open-drawer]',
5986 headerSearchInput: '[data-predictive-search-drawer-input]',
5987 predictiveSearchWrapper: '[data-predictive-search-mount="drawer"]'
5988 };
5989
5990 var drawerInstance;
5991
5992 function init() {
5993 setAccessibilityProps();
5994
5995 drawerInstance = new theme.Drawers('SearchDrawer', 'top', {
5996 onDrawerOpen: function() {
5997 setHeight();
5998 theme.MobileNav.closeMobileNav();
5999 lockBodyScroll();
6000 },
6001 onDrawerClose: function() {
6002 theme.SearchHeader.clearAndClose();
6003 var drawerOpenButton = document.querySelector(
6004 selectors.drawerOpenButton
6005 );
6006
6007 if (drawerOpenButton) drawerOpenButton.focus();
6008
6009 unlockBodyScroll();
6010 },
6011 withPredictiveSearch: true,
6012 elementToFocusOnOpen: document.querySelector(selectors.headerSearchInput)
6013 });
6014 }
6015
6016 function setAccessibilityProps() {
6017 var drawerOpenButton = document.querySelector(selectors.drawerOpenButton);
6018
6019 if (drawerOpenButton) {
6020 drawerOpenButton.setAttribute('aria-controls', 'SearchDrawer');
6021 drawerOpenButton.setAttribute('aria-expanded', 'false');
6022 drawerOpenButton.setAttribute('aria-controls', 'dialog');
6023 }
6024 }
6025
6026 function setHeight() {
6027 var searchDrawer = document.querySelector(selectors.drawer);
6028 var headerHeight = document.querySelector(selectors.headerSection)
6029 .offsetHeight;
6030
6031 searchDrawer.style.height = headerHeight + 'px';
6032 }
6033
6034 function close() {
6035 drawerInstance.close();
6036 }
6037
6038 function lockBodyScroll() {
6039 theme.Helpers.enableScrollLock();
6040 }
6041
6042 function unlockBodyScroll() {
6043 theme.Helpers.disableScrollLock();
6044 }
6045
6046 return {
6047 init: init,
6048 close: close
6049 };
6050})();
6051
6052theme.Disclosure = (function() {
6053 var selectors = {
6054 disclosureForm: '[data-disclosure-form]',
6055 disclosureList: '[data-disclosure-list]',
6056 disclosureToggle: '[data-disclosure-toggle]',
6057 disclosureInput: '[data-disclosure-input]',
6058 disclosureOptions: '[data-disclosure-option]'
6059 };
6060
6061 var classes = {
6062 listVisible: 'disclosure-list--visible'
6063 };
6064
6065 function Disclosure(disclosure) {
6066 this.container = disclosure;
6067 this._cacheSelectors();
6068 this._setupListeners();
6069 }
6070
6071 Disclosure.prototype = Object.assign({}, Disclosure.prototype, {
6072 _cacheSelectors: function() {
6073 this.cache = {
6074 disclosureForm: this.container.closest(selectors.disclosureForm),
6075 disclosureList: this.container.querySelector(selectors.disclosureList),
6076 disclosureToggle: this.container.querySelector(
6077 selectors.disclosureToggle
6078 ),
6079 disclosureInput: this.container.querySelector(
6080 selectors.disclosureInput
6081 ),
6082 disclosureOptions: this.container.querySelectorAll(
6083 selectors.disclosureOptions
6084 )
6085 };
6086 },
6087
6088 _setupListeners: function() {
6089 this.eventHandlers = this._setupEventHandlers();
6090
6091 this.cache.disclosureToggle.addEventListener(
6092 'click',
6093 this.eventHandlers.toggleList
6094 );
6095
6096 this.cache.disclosureOptions.forEach(function(disclosureOption) {
6097 disclosureOption.addEventListener(
6098 'click',
6099 this.eventHandlers.connectOptions
6100 );
6101 }, this);
6102
6103 this.container.addEventListener(
6104 'keyup',
6105 this.eventHandlers.onDisclosureKeyUp
6106 );
6107
6108 this.cache.disclosureList.addEventListener(
6109 'focusout',
6110 this.eventHandlers.onDisclosureListFocusOut
6111 );
6112
6113 this.cache.disclosureToggle.addEventListener(
6114 'focusout',
6115 this.eventHandlers.onDisclosureToggleFocusOut
6116 );
6117
6118 document.body.addEventListener('click', this.eventHandlers.onBodyClick);
6119 },
6120
6121 _setupEventHandlers: function() {
6122 return {
6123 connectOptions: this._connectOptions.bind(this),
6124 toggleList: this._toggleList.bind(this),
6125 onBodyClick: this._onBodyClick.bind(this),
6126 onDisclosureKeyUp: this._onDisclosureKeyUp.bind(this),
6127 onDisclosureListFocusOut: this._onDisclosureListFocusOut.bind(this),
6128 onDisclosureToggleFocusOut: this._onDisclosureToggleFocusOut.bind(this)
6129 };
6130 },
6131
6132 _connectOptions: function(event) {
6133 event.preventDefault();
6134
6135 this._submitForm(event.currentTarget.dataset.value);
6136 },
6137
6138 _onDisclosureToggleFocusOut: function(event) {
6139 var disclosureLostFocus =
6140 this.container.contains(event.relatedTarget) === false;
6141
6142 if (disclosureLostFocus) {
6143 this._hideList();
6144 }
6145 },
6146
6147 _onDisclosureListFocusOut: function(event) {
6148 var childInFocus = event.currentTarget.contains(event.relatedTarget);
6149
6150 var isVisible = this.cache.disclosureList.classList.contains(
6151 classes.listVisible
6152 );
6153
6154 if (isVisible && !childInFocus) {
6155 this._hideList();
6156 }
6157 },
6158
6159 _onDisclosureKeyUp: function(event) {
6160 if (event.which !== slate.utils.keyboardKeys.ESCAPE) return;
6161 this._hideList();
6162 this.cache.disclosureToggle.focus();
6163 },
6164
6165 _onBodyClick: function(event) {
6166 var isOption = this.container.contains(event.target);
6167 var isVisible = this.cache.disclosureList.classList.contains(
6168 classes.listVisible
6169 );
6170
6171 if (isVisible && !isOption) {
6172 this._hideList();
6173 }
6174 },
6175
6176 _submitForm: function(value) {
6177 this.cache.disclosureInput.value = value;
6178 this.cache.disclosureForm.submit();
6179 },
6180
6181 _hideList: function() {
6182 this.cache.disclosureList.classList.remove(classes.listVisible);
6183 this.cache.disclosureToggle.setAttribute('aria-expanded', false);
6184 },
6185
6186 _toggleList: function() {
6187 var ariaExpanded =
6188 this.cache.disclosureToggle.getAttribute('aria-expanded') === 'true';
6189 this.cache.disclosureList.classList.toggle(classes.listVisible);
6190 this.cache.disclosureToggle.setAttribute('aria-expanded', !ariaExpanded);
6191 },
6192
6193 destroy: function() {
6194 this.cache.disclosureToggle.removeEventListener(
6195 'click',
6196 this.eventHandlers.toggleList
6197 );
6198
6199 this.cache.disclosureOptions.forEach(function(disclosureOption) {
6200 disclosureOption.removeEventListener(
6201 'click',
6202 this.eventHandlers.connectOptions
6203 );
6204 }, this);
6205
6206 this.container.removeEventListener(
6207 'keyup',
6208 this.eventHandlers.onDisclosureKeyUp
6209 );
6210
6211 this.cache.disclosureList.removeEventListener(
6212 'focusout',
6213 this.eventHandlers.onDisclosureListFocusOut
6214 );
6215
6216 this.cache.disclosureToggle.removeEventListener(
6217 'focusout',
6218 this.eventHandlers.onDisclosureToggleFocusOut
6219 );
6220
6221 document.body.removeEventListener(
6222 'click',
6223 this.eventHandlers.onBodyClick
6224 );
6225 }
6226 });
6227
6228 return Disclosure;
6229})();
6230
6231theme.Zoom = (function() {
6232 var selectors = {
6233 imageZoom: '[data-image-zoom]'
6234 };
6235
6236 var classes = {
6237 zoomImg: 'zoomImg'
6238 };
6239
6240 var attributes = {
6241 imageZoomTarget: 'data-image-zoom-target'
6242 };
6243
6244 function Zoom(container) {
6245 this.container = container;
6246 this.cache = {};
6247 this.url = container.dataset.zoom;
6248
6249 this._cacheSelectors();
6250
6251 if (!this.cache.sourceImage) return;
6252
6253 this._duplicateImage();
6254 }
6255
6256 Zoom.prototype = Object.assign({}, Zoom.prototype, {
6257 _cacheSelectors: function() {
6258 this.cache = {
6259 sourceImage: this.container.querySelector(selectors.imageZoom)
6260 };
6261 },
6262
6263 _init: function() {
6264 var targetWidth = this.cache.targetImage.width;
6265 var targetHeight = this.cache.targetImage.height;
6266
6267 if (this.cache.sourceImage === this.cache.targetImage) {
6268 this.sourceWidth = targetWidth;
6269 this.sourceHeight = targetHeight;
6270 } else {
6271 this.sourceWidth = this.cache.sourceImage.width;
6272 this.sourceHeight = this.cache.sourceImage.height;
6273 }
6274
6275 this.xRatio =
6276 (this.cache.sourceImage.width - targetWidth) / this.sourceWidth;
6277 this.yRatio =
6278 (this.cache.sourceImage.height - targetHeight) / this.sourceHeight;
6279 },
6280
6281 _start: function(e) {
6282 this._init();
6283 this._move(e);
6284 },
6285
6286 _stop: function() {
6287 this.cache.targetImage.style.opacity = 0;
6288 },
6289
6290 /**
6291 * Sets the correct coordinates top and left position in px
6292 * It sets a limit within between 0 and the max height of the image
6293 * So when the mouse leaves the target image, it could
6294 * never go above or beyond the target image zone
6295 */
6296 _setTopLeftMaxValues: function(top, left) {
6297 return {
6298 left: Math.max(Math.min(left, this.sourceWidth), 0),
6299 top: Math.max(Math.min(top, this.sourceHeight), 0)
6300 };
6301 },
6302
6303 _move: function(e) {
6304 // get left and top position within the "source image" zone
6305 var left =
6306 e.pageX -
6307 (this.cache.sourceImage.getBoundingClientRect().left + window.scrollX);
6308 var top =
6309 e.pageY -
6310 (this.cache.sourceImage.getBoundingClientRect().top + window.scrollY);
6311 // make sure the left and top position don't go
6312 // above or beyond the target image zone
6313 var position = this._setTopLeftMaxValues(top, left);
6314
6315 top = position.top;
6316 left = position.left;
6317
6318 this.cache.targetImage.style.left = -(left * -this.xRatio) + 'px';
6319 this.cache.targetImage.style.top = -(top * -this.yRatio) + 'px';
6320 this.cache.targetImage.style.opacity = 1;
6321 },
6322
6323 /**
6324 * This loads a high resolution image
6325 * via the data attributes url
6326 * It adds all necessary CSS styles and adds to the container
6327 */
6328 _duplicateImage: function() {
6329 this._loadImage()
6330 .then(
6331 function(image) {
6332 this.cache.targetImage = image;
6333 image.style.width = image.width + 'px';
6334 image.style.height = image.height + 'px';
6335 image.style.position = 'absolute';
6336 image.style.maxWidth = 'none';
6337 image.style.maxHeight = 'none';
6338 image.style.opacity = 0;
6339 image.style.border = 'none';
6340 image.style.left = 0;
6341 image.style.top = 0;
6342
6343 this.container.appendChild(image);
6344
6345 this._init();
6346
6347 this._start = this._start.bind(this);
6348 this._stop = this._stop.bind(this);
6349 this._move = this._move.bind(this);
6350
6351 this.container.addEventListener('mouseenter', this._start);
6352 this.container.addEventListener('mouseleave', this._stop);
6353 this.container.addEventListener('mousemove', this._move);
6354
6355 this.container.style.position = 'relative';
6356 this.container.style.overflow = 'hidden';
6357 }.bind(this)
6358 )
6359 .catch(function(error) {
6360 // eslint-disable-next-line no-console
6361 console.warn('Error fetching image', error);
6362 });
6363 },
6364
6365 _loadImage: function() {
6366 // eslint-disable-next-line
6367 return new Promise(function(resolve, reject) {
6368 var image = new Image();
6369 image.setAttribute('role', 'presentation');
6370 image.setAttribute(attributes.imageZoomTarget, true);
6371 image.classList.add(classes.zoomImg);
6372 image.src = this.url;
6373
6374 image.addEventListener('load', function() {
6375 resolve(image);
6376 });
6377
6378 image.addEventListener('error', function(error) {
6379 reject(error);
6380 });
6381 }.bind(this)
6382 );
6383 },
6384
6385 unload: function() {
6386 var targetImage = this.container.querySelector(
6387 '[' + attributes.imageZoomTarget + ']'
6388 );
6389 if (targetImage) {
6390 targetImage.remove();
6391 }
6392
6393 this.container.removeEventListener('mouseenter', this._start);
6394 this.container.removeEventListener('mouseleave', this._stop);
6395 this.container.removeEventListener('mousemove', this._move);
6396 }
6397 });
6398
6399 return Zoom;
6400})();
6401
6402
6403/* ================ TEMPLATES ================ */
6404(function() {
6405 var filterBys = document.querySelectorAll('[data-blog-tag-filter]');
6406
6407 if (!filterBys.length) return;
6408
6409 slate.utils.resizeSelects(filterBys);
6410
6411 filterBys.forEach(function(filterBy) {
6412 filterBy.addEventListener('change', function(evt) {
6413 location.href = evt.target.value;
6414 });
6415 });
6416})();
6417
6418window.theme = theme || {};
6419
6420theme.customerTemplates = (function() {
6421 var selectors = {
6422 RecoverHeading: '#RecoverHeading',
6423 RecoverEmail: '#RecoverEmail',
6424 LoginHeading: '#LoginHeading'
6425 };
6426
6427 function initEventListeners() {
6428 this.recoverHeading = document.querySelector(selectors.RecoverHeading);
6429 this.recoverEmail = document.querySelector(selectors.RecoverEmail);
6430 this.loginHeading = document.querySelector(selectors.LoginHeading);
6431 var recoverPassword = document.getElementById('RecoverPassword');
6432 var hideRecoverPasswordLink = document.getElementById(
6433 'HideRecoverPasswordLink'
6434 );
6435
6436 // Show reset password form
6437 if (recoverPassword) {
6438 recoverPassword.addEventListener(
6439 'click',
6440 function(evt) {
6441 evt.preventDefault();
6442 showRecoverPasswordForm();
6443 this.recoverHeading.setAttribute('tabindex', '-1');
6444 this.recoverHeading.focus();
6445 }.bind(this)
6446 );
6447 }
6448
6449 // Hide reset password form
6450 if (hideRecoverPasswordLink) {
6451 hideRecoverPasswordLink.addEventListener(
6452 'click',
6453 function(evt) {
6454 evt.preventDefault();
6455 hideRecoverPasswordForm();
6456 this.loginHeading.setAttribute('tabindex', '-1');
6457 this.loginHeading.focus();
6458 }.bind(this)
6459 );
6460 }
6461
6462 if (this.recoverHeading) {
6463 this.recoverHeading.addEventListener('blur', function(evt) {
6464 evt.target.removeAttribute('tabindex');
6465 });
6466 }
6467
6468 if (this.loginHeading) {
6469 this.loginHeading.addEventListener('blur', function(evt) {
6470 evt.target.removeAttribute('tabindex');
6471 });
6472 }
6473 }
6474
6475 /**
6476 *
6477 * Show/Hide recover password form
6478 *
6479 */
6480
6481 function showRecoverPasswordForm() {
6482 document.getElementById('RecoverPasswordForm').classList.remove('hide');
6483 document.getElementById('CustomerLoginForm').classList.add('hide');
6484
6485 if (this.recoverEmail.getAttribute('aria-invalid') === 'true') {
6486 this.recoverEmail.focus();
6487 }
6488 }
6489
6490 function hideRecoverPasswordForm() {
6491 document.getElementById('RecoverPasswordForm').classList.add('hide');
6492 document.getElementById('CustomerLoginForm').classList.remove('hide');
6493 }
6494
6495 /**
6496 *
6497 * Show reset password success message
6498 *
6499 */
6500 function resetPasswordSuccess() {
6501 var formState = document.querySelector('.reset-password-success');
6502
6503 // check if reset password form was successfully submited.
6504 if (!formState) {
6505 return;
6506 }
6507
6508 // show success message
6509 var resetSuccess = document.getElementById('ResetSuccess');
6510 resetSuccess.classList.remove('hide');
6511 resetSuccess.focus();
6512 }
6513
6514 /**
6515 *
6516 * Show/hide customer address forms
6517 *
6518 */
6519 function customerAddressForm() {
6520 var newAddressForm = document.getElementById('AddressNewForm');
6521 var newAddressFormButton = document.getElementById('AddressNewButton');
6522
6523 if (!newAddressForm) {
6524 return;
6525 }
6526
6527 // Initialize observers on address selectors, defined in shopify_common.js
6528 if (Shopify) {
6529 // eslint-disable-next-line no-new
6530 new Shopify.CountryProvinceSelector(
6531 'AddressCountryNew',
6532 'AddressProvinceNew',
6533 {
6534 hideElement: 'AddressProvinceContainerNew'
6535 }
6536 );
6537 }
6538
6539 // Initialize each edit form's country/province selector
6540 document
6541 .querySelectorAll('.address-country-option')
6542 .forEach(function(option) {
6543 var formId = option.dataset.formId;
6544 var countrySelector = 'AddressCountry_' + formId;
6545 var provinceSelector = 'AddressProvince_' + formId;
6546 var containerSelector = 'AddressProvinceContainer_' + formId;
6547
6548 // eslint-disable-next-line no-new
6549 new Shopify.CountryProvinceSelector(countrySelector, provinceSelector, {
6550 hideElement: containerSelector
6551 });
6552 });
6553
6554 // Toggle new/edit address forms
6555 document.querySelectorAll('.address-new-toggle').forEach(function(button) {
6556 button.addEventListener('click', function() {
6557 var isExpanded =
6558 newAddressFormButton.getAttribute('aria-expanded') === 'true';
6559
6560 newAddressForm.classList.toggle('hide');
6561 newAddressFormButton.setAttribute('aria-expanded', !isExpanded);
6562 newAddressFormButton.focus();
6563 });
6564 });
6565
6566 document.querySelectorAll('.address-edit-toggle').forEach(function(button) {
6567 button.addEventListener('click', function(evt) {
6568 var formId = evt.target.dataset.formId;
6569 var editButton = document.getElementById('EditFormButton_' + formId);
6570 var editAddress = document.getElementById('EditAddress_' + formId);
6571 var isExpanded = editButton.getAttribute('aria-expanded') === 'true';
6572
6573 editAddress.classList.toggle('hide');
6574 editButton.setAttribute('aria-expanded', !isExpanded);
6575 editButton.focus();
6576 });
6577 });
6578
6579 document.querySelectorAll('.address-delete').forEach(function(button) {
6580 button.addEventListener('click', function(evt) {
6581 var target = evt.target.dataset.target;
6582 var confirmMessage = evt.target.dataset.confirmMessage;
6583
6584 // eslint-disable-next-line no-alert
6585 if (
6586 confirm(
6587 confirmMessage || 'Are you sure you wish to delete this address?'
6588 )
6589 ) {
6590 Shopify.postLink(target, {
6591 parameters: { _method: 'delete' }
6592 });
6593 }
6594 });
6595 });
6596 }
6597
6598 /**
6599 *
6600 * Check URL for reset password hash
6601 *
6602 */
6603 function checkUrlHash() {
6604 var hash = window.location.hash;
6605
6606 // Allow deep linking to recover password form
6607 if (hash === '#recover') {
6608 showRecoverPasswordForm.bind(this)();
6609 }
6610 }
6611
6612 return {
6613 init: function() {
6614 initEventListeners();
6615 checkUrlHash();
6616 resetPasswordSuccess();
6617 customerAddressForm();
6618 }
6619 };
6620})();
6621
6622
6623/*================ SECTIONS ================*/
6624window.theme = window.theme || {};
6625
6626theme.Cart = (function() {
6627 var selectors = {
6628 cartCount: '[data-cart-count]',
6629 cartCountBubble: '[data-cart-count-bubble]',
6630 cartDiscount: '[data-cart-discount]',
6631 cartDiscountTitle: '[data-cart-discount-title]',
6632 cartDiscountAmount: '[data-cart-discount-amount]',
6633 cartDiscountWrapper: '[data-cart-discount-wrapper]',
6634 cartErrorMessage: '[data-cart-error-message]',
6635 cartErrorMessageWrapper: '[data-cart-error-message-wrapper]',
6636 cartItem: '[data-cart-item]',
6637 cartItemDetails: '[data-cart-item-details]',
6638 cartItemDiscount: '[data-cart-item-discount]',
6639 cartItemDiscountedPriceGroup: '[data-cart-item-discounted-price-group]',
6640 cartItemDiscountTitle: '[data-cart-item-discount-title]',
6641 cartItemDiscountAmount: '[data-cart-item-discount-amount]',
6642 cartItemDiscountList: '[data-cart-item-discount-list]',
6643 cartItemFinalPrice: '[data-cart-item-final-price]',
6644 cartItemImage: '[data-cart-item-image]',
6645 cartItemLinePrice: '[data-cart-item-line-price]',
6646 cartItemOriginalPrice: '[data-cart-item-original-price]',
6647 cartItemPrice: '[data-cart-item-price]',
6648 cartItemPriceList: '[data-cart-item-price-list]',
6649 cartItemProperty: '[data-cart-item-property]',
6650 cartItemPropertyName: '[data-cart-item-property-name]',
6651 cartItemPropertyValue: '[data-cart-item-property-value]',
6652 cartItemRegularPriceGroup: '[data-cart-item-regular-price-group]',
6653 cartItemRegularPrice: '[data-cart-item-regular-price]',
6654 cartItemTitle: '[data-cart-item-title]',
6655 cartItemOption: '[data-cart-item-option]',
6656 cartItemSellingPlanName: '[data-cart-item-selling-plan-name]',
6657 cartLineItems: '[data-cart-line-items]',
6658 cartNote: '[data-cart-notes]',
6659 cartQuantityErrorMessage: '[data-cart-quantity-error-message]',
6660 cartQuantityErrorMessageWrapper:
6661 '[data-cart-quantity-error-message-wrapper]',
6662 cartRemove: '[data-cart-remove]',
6663 cartStatus: '[data-cart-status]',
6664 cartSubtotal: '[data-cart-subtotal]',
6665 cartTableCell: '[data-cart-table-cell]',
6666 cartWrapper: '[data-cart-wrapper]',
6667 emptyPageContent: '[data-empty-page-content]',
6668 quantityInput: '[data-quantity-input]',
6669 quantityInputMobile: '[data-quantity-input-mobile]',
6670 quantityInputDesktop: '[data-quantity-input-desktop]',
6671 quantityLabelMobile: '[data-quantity-label-mobile]',
6672 quantityLabelDesktop: '[data-quantity-label-desktop]',
6673 inputQty: '[data-quantity-input]',
6674 thumbnails: '.cart__image',
6675 unitPrice: '[data-unit-price]',
6676 unitPriceBaseUnit: '[data-unit-price-base-unit]',
6677 unitPriceGroup: '[data-unit-price-group]'
6678 };
6679
6680 var classes = {
6681 cartNoCookies: 'cart--no-cookies',
6682 cartRemovedProduct: 'cart__removed-product',
6683 thumbnails: 'cart__image',
6684 hide: 'hide',
6685 inputError: 'input--error'
6686 };
6687
6688 var attributes = {
6689 cartItemIndex: 'data-cart-item-index',
6690 cartItemKey: 'data-cart-item-key',
6691 cartItemQuantity: 'data-cart-item-quantity',
6692 cartItemTitle: 'data-cart-item-title',
6693 cartItemUrl: 'data-cart-item-url',
6694 quantityItem: 'data-quantity-item'
6695 };
6696
6697 var mediumUpQuery = '(min-width: ' + theme.breakpoints.medium + 'px)';
6698
6699 function Cart(container) {
6700 this.container = container;
6701 this.thumbnails = this.container.querySelectorAll(selectors.thumbnails);
6702 this.quantityInputs = this.container.querySelectorAll(selectors.inputQty);
6703 this.ajaxEnabled =
6704 this.container.getAttribute('data-ajax-enabled') === 'true';
6705
6706 this._handleInputQty = theme.Helpers.debounce(
6707 this._handleInputQty.bind(this),
6708 500
6709 );
6710 this.setQuantityFormControllers = this.setQuantityFormControllers.bind(
6711 this
6712 );
6713 this._onNoteChange = this._onNoteChange.bind(this);
6714 this._onRemoveItem = this._onRemoveItem.bind(this);
6715
6716 if (!theme.Helpers.cookiesEnabled()) {
6717 this.container.classList.add(classes.cartNoCookies);
6718 }
6719
6720 this.thumbnails.forEach(function(element) {
6721 element.style.cursor = 'pointer';
6722 });
6723
6724 this.container.addEventListener('click', this._handleThumbnailClick);
6725 this.container.addEventListener('change', this._handleInputQty);
6726
6727 this.mql = window.matchMedia(mediumUpQuery);
6728 this.mql.addListener(this.setQuantityFormControllers);
6729
6730 this.setQuantityFormControllers();
6731
6732 if (this.ajaxEnabled) {
6733 /**
6734 * Because the entire cart is recreated when a cart item is updated,
6735 * we cannot cache the elements in the cart. Instead, we add the event
6736 * listeners on the cart's container to allow us to retain the event
6737 * listeners after rebuilding the cart when an item is updated.
6738 */
6739 this.container.addEventListener('click', this._onRemoveItem);
6740 this.container.addEventListener('change', this._onNoteChange);
6741
6742 this._setupCartTemplates();
6743 }
6744 }
6745
6746 Cart.prototype = Object.assign({}, Cart.prototype, {
6747 _setupCartTemplates: function() {
6748 var cartItem = this.container.querySelector(selectors.cartItem);
6749 if (!cartItem) return;
6750
6751 this.itemTemplate = this.container
6752 .querySelector(selectors.cartItem)
6753 .cloneNode(true);
6754
6755 this.itemDiscountTemplate = this.itemTemplate
6756 .querySelector(selectors.cartItemDiscount)
6757 .cloneNode(true);
6758
6759 this.cartDiscountTemplate = this.container
6760 .querySelector(selectors.cartDiscount)
6761 .cloneNode(true);
6762
6763 this.itemPriceListTemplate = this.itemTemplate
6764 .querySelector(selectors.cartItemPriceList)
6765 .cloneNode(true);
6766
6767 this.itemOptionTemplate = this.itemTemplate
6768 .querySelector(selectors.cartItemOption)
6769 .cloneNode(true);
6770
6771 this.itemPropertyTemplate = this.itemTemplate
6772 .querySelector(selectors.cartItemProperty)
6773 .cloneNode(true);
6774
6775 this.itemSellingPlanNameTemplate = this.itemTemplate
6776 .querySelector(selectors.cartItemSellingPlanName)
6777 .cloneNode(true);
6778 },
6779
6780 _handleInputQty: function(evt) {
6781 if (!evt.target.hasAttribute('data-quantity-input')) return;
6782
6783 var input = evt.target;
6784 var itemElement = input.closest(selectors.cartItem);
6785
6786 var itemIndex = Number(input.getAttribute('data-quantity-item'));
6787
6788 var itemQtyInputs = this.container.querySelectorAll(
6789 "[data-quantity-item='" + itemIndex + "']"
6790 );
6791
6792 var value = parseInt(input.value);
6793
6794 var isValidValue = !(value < 0 || isNaN(value));
6795
6796 itemQtyInputs.forEach(function(element) {
6797 element.value = value;
6798 });
6799
6800 this._hideCartError();
6801 this._hideQuantityErrorMessage();
6802
6803 if (!isValidValue) {
6804 this._showQuantityErrorMessages(itemElement);
6805 return;
6806 }
6807
6808 if (isValidValue && this.ajaxEnabled) {
6809 this._updateItemQuantity(itemIndex, itemElement, itemQtyInputs, value);
6810 }
6811 },
6812
6813 _updateItemQuantity: function(
6814 itemIndex,
6815 itemElement,
6816 itemQtyInputs,
6817 value
6818 ) {
6819 var key = itemElement.getAttribute(attributes.cartItemKey);
6820 var index = Number(itemElement.getAttribute(attributes.cartItemIndex));
6821
6822 var request = {
6823 method: 'POST',
6824 headers: {
6825 'Content-Type': 'application/json;'
6826 },
6827 body: JSON.stringify({
6828 line: index,
6829 quantity: value
6830 })
6831 };
6832
6833 fetch('/cart/change.js', request)
6834 .then(function(response) {
6835 return response.json();
6836 })
6837 .then(
6838 function(state) {
6839 this._setCartCountBubble(state.item_count);
6840
6841 if (!state.item_count) {
6842 this._emptyCart();
6843 return;
6844 }
6845
6846 this._createCart(state);
6847
6848 if (!value) {
6849 this._showRemoveMessage(itemElement.cloneNode(true));
6850 return;
6851 }
6852
6853 var lineItem = document.querySelector(
6854 "[data-cart-item-key='" + key + "']"
6855 );
6856
6857 var item = this.getItem(key, state);
6858
6859 var inputSelector = this.mql.matches
6860 ? selectors.quantityInputDesktop
6861 : selectors.quantityInputMobile;
6862
6863 this._updateLiveRegion(item);
6864
6865 if (!lineItem) return;
6866 lineItem.querySelector(inputSelector).focus();
6867 }.bind(this)
6868 )
6869 .catch(
6870 function() {
6871 this._showCartError(null);
6872 }.bind(this)
6873 );
6874 },
6875
6876 getItem: function(key, state) {
6877 return state.items.find(function(item) {
6878 return item.key === key;
6879 });
6880 },
6881
6882 _liveRegionText: function(item) {
6883 // Dummy content for live region
6884 var liveRegionText =
6885 theme.strings.update +
6886 ': [QuantityLabel]: [Quantity], [Regular] [$$] [DiscountedPrice] [$]. [PriceInformation]';
6887
6888 // Update Quantity
6889 liveRegionText = liveRegionText
6890 .replace('[QuantityLabel]', theme.strings.quantity)
6891 .replace('[Quantity]', item.quantity);
6892
6893 // Update pricing information
6894 var regularLabel = '';
6895 var regularPrice = theme.Currency.formatMoney(
6896 item.original_line_price,
6897 theme.moneyFormat
6898 );
6899 var discountLabel = '';
6900 var discountPrice = '';
6901 var discountInformation = '';
6902
6903 if (item.original_line_price > item.final_line_price) {
6904 regularLabel = theme.strings.regularTotal;
6905
6906 discountLabel = theme.strings.discountedTotal;
6907 discountPrice = theme.Currency.formatMoney(
6908 item.final_line_price,
6909 theme.moneyFormat
6910 );
6911
6912 discountInformation = theme.strings.priceColumn;
6913 }
6914
6915 liveRegionText = liveRegionText
6916 .replace('[Regular]', regularLabel)
6917 .replace('[$$]', regularPrice)
6918 .replace('[DiscountedPrice]', discountLabel)
6919 .replace('[$]', discountPrice)
6920 .replace('[PriceInformation]', discountInformation)
6921 .trim();
6922
6923 return liveRegionText;
6924 },
6925
6926 _updateLiveRegion: function(item) {
6927 if (!item) return;
6928
6929 var liveRegion = this.container.querySelector(selectors.cartStatus);
6930
6931 liveRegion.textContent = this._liveRegionText(item);
6932 liveRegion.setAttribute('aria-hidden', false);
6933
6934 setTimeout(function() {
6935 liveRegion.setAttribute('aria-hidden', true);
6936 }, 1000);
6937 },
6938
6939 _createCart: function(state) {
6940 var cartDiscountList = this._createCartDiscountList(state);
6941
6942 var cartTable = this.container.querySelector(selectors.cartLineItems);
6943 cartTable.innerHTML = '';
6944
6945 this._createLineItemList(state).forEach(function(lineItem) {
6946 cartTable.appendChild(lineItem);
6947 });
6948
6949 this.setQuantityFormControllers();
6950
6951 this.cartNotes =
6952 this.cartNotes || this.container.querySelector(selectors.cartNote);
6953
6954 if (this.cartNotes) {
6955 this.cartNotes.value = state.note;
6956 }
6957
6958 var discountWrapper = this.container.querySelector(
6959 selectors.cartDiscountWrapper
6960 );
6961
6962 if (cartDiscountList.length === 0) {
6963 discountWrapper.innerHTML = '';
6964 discountWrapper.classList.add(classes.hide);
6965 } else {
6966 discountWrapper.innerHTML = '';
6967
6968 cartDiscountList.forEach(function(discountItem) {
6969 discountWrapper.appendChild(discountItem);
6970 });
6971
6972 discountWrapper.classList.remove(classes.hide);
6973 }
6974
6975 this.container.querySelector(
6976 selectors.cartSubtotal
6977 ).innerHTML = theme.Currency.formatMoney(
6978 state.total_price,
6979 theme.moneyFormatWithCurrency
6980 );
6981 },
6982
6983 _createCartDiscountList: function(cart) {
6984 return cart.cart_level_discount_applications.map(
6985 function(discount) {
6986 var discountNode = this.cartDiscountTemplate.cloneNode(true);
6987
6988 discountNode.querySelector(selectors.cartDiscountTitle).textContent =
6989 discount.title;
6990
6991 discountNode.querySelector(
6992 selectors.cartDiscountAmount
6993 ).innerHTML = theme.Currency.formatMoney(
6994 discount.total_allocated_amount,
6995 theme.moneyFormat
6996 );
6997
6998 return discountNode;
6999 }.bind(this)
7000 );
7001 },
7002
7003 _createLineItemList: function(state) {
7004 return state.items.map(
7005 function(item, index) {
7006 var itemNode = this.itemTemplate.cloneNode(true);
7007
7008 var itemPriceList = this.itemPriceListTemplate.cloneNode(true);
7009
7010 this._setLineItemAttributes(itemNode, item, index);
7011 this._setLineItemImage(itemNode, item.featured_image);
7012
7013 var cartItemTitle = itemNode.querySelector(selectors.cartItemTitle);
7014 cartItemTitle.textContent = item.product_title;
7015 cartItemTitle.setAttribute('href', item.url);
7016
7017 var selling_plan_name = item.selling_plan_allocation
7018 ? item.selling_plan_allocation.selling_plan.name
7019 : null;
7020
7021 var productDetailsList = this._createProductDetailsList(
7022 item.product_has_only_default_variant,
7023 item.options_with_values,
7024 item.properties,
7025 selling_plan_name
7026 );
7027
7028 this._setProductDetailsList(itemNode, productDetailsList);
7029
7030 this._setItemRemove(itemNode, item.title);
7031
7032 itemPriceList.innerHTML = this._createItemPrice(
7033 item.original_price,
7034 item.final_price
7035 ).outerHTML;
7036
7037 if (item.unit_price_measurement) {
7038 itemPriceList.appendChild(
7039 this._createUnitPrice(
7040 item.unit_price,
7041 item.unit_price_measurement
7042 )
7043 );
7044 }
7045
7046 this._setItemPrice(itemNode, itemPriceList);
7047
7048 var itemDiscountList = this._createItemDiscountList(item);
7049 this._setItemDiscountList(itemNode, itemDiscountList);
7050 this._setQuantityInputs(itemNode, item, index);
7051
7052 var itemLinePrice = this._createItemPrice(
7053 item.original_line_price,
7054 item.final_line_price
7055 );
7056
7057 this._setItemLinePrice(itemNode, itemLinePrice);
7058
7059 return itemNode;
7060 }.bind(this)
7061 );
7062 },
7063
7064 _setLineItemAttributes: function(itemNode, item, index) {
7065 itemNode.setAttribute(attributes.cartItemKey, item.key);
7066 itemNode.setAttribute(attributes.cartItemUrl, item.url);
7067 itemNode.setAttribute(attributes.cartItemTitle, item.title);
7068 itemNode.setAttribute(attributes.cartItemIndex, index + 1);
7069 itemNode.setAttribute(attributes.cartItemQuantity, item.quantity);
7070 },
7071
7072 _setLineItemImage: function(itemNode, featuredImage) {
7073 var image = itemNode.querySelector(selectors.cartItemImage);
7074
7075 var sizedImageUrl =
7076 featuredImage.url !== null
7077 ? theme.Images.getSizedImageUrl(featuredImage.url, 'x190')
7078 : null;
7079
7080 if (sizedImageUrl) {
7081 image.setAttribute('alt', featuredImage.alt);
7082 image.setAttribute('src', sizedImageUrl);
7083 image.classList.remove(classes.hide);
7084 } else {
7085 image.parentNode.removeChild(image);
7086 }
7087 },
7088
7089 _setProductDetailsList: function(item, productDetailsList) {
7090 var itemDetails = item.querySelector(selectors.cartItemDetails);
7091
7092 if (productDetailsList.length) {
7093 itemDetails.classList.remove(classes.hide);
7094 itemDetails.innerHTML = productDetailsList.reduce(function(
7095 result,
7096 element
7097 ) {
7098 return result + element.outerHTML;
7099 },
7100 '');
7101
7102 return;
7103 }
7104
7105 itemDetails.classList.add(classes.hide);
7106 itemDetails.textContent = '';
7107 },
7108
7109 _setItemPrice: function(item, price) {
7110 item.querySelector(selectors.cartItemPrice).innerHTML = price.outerHTML;
7111 },
7112
7113 _setItemDiscountList: function(item, discountList) {
7114 var itemDiscountList = item.querySelector(selectors.cartItemDiscountList);
7115
7116 if (discountList.length === 0) {
7117 itemDiscountList.innerHTML = '';
7118 itemDiscountList.classList.add(classes.hide);
7119 } else {
7120 itemDiscountList.innerHTML = discountList.reduce(function(
7121 result,
7122 element
7123 ) {
7124 return result + element.outerHTML;
7125 },
7126 '');
7127
7128 itemDiscountList.classList.remove(classes.hide);
7129 }
7130 },
7131
7132 _setItemRemove: function(item, title) {
7133 item
7134 .querySelector(selectors.cartRemove)
7135 .setAttribute(
7136 'aria-label',
7137 theme.strings.removeLabel.replace('[product]', title)
7138 );
7139 },
7140
7141 _setQuantityInputs: function(itemNode, item, index) {
7142 var mobileInput = itemNode.querySelector(selectors.quantityInputMobile);
7143 var desktopInput = itemNode.querySelector(selectors.quantityInputDesktop);
7144
7145 mobileInput.setAttribute('id', 'updates_' + item.key);
7146 desktopInput.setAttribute('id', 'updates_large_' + item.key);
7147
7148 [mobileInput, desktopInput].forEach(function(element) {
7149 element.setAttribute(attributes.quantityItem, index + 1);
7150 element.value = item.quantity;
7151 });
7152
7153 itemNode
7154 .querySelector(selectors.quantityLabelMobile)
7155 .setAttribute('for', 'updates_' + item.key);
7156
7157 itemNode
7158 .querySelector(selectors.quantityLabelDesktop)
7159 .setAttribute('for', 'updates_large_' + item.key);
7160 },
7161
7162 setQuantityFormControllers: function() {
7163 var desktopQuantityInputs = document.querySelectorAll(
7164 selectors.quantityInputDesktop
7165 );
7166
7167 var mobileQuantityInputs = document.querySelectorAll(
7168 selectors.quantityInputMobile
7169 );
7170
7171 if (this.mql.matches) {
7172 addNameAttribute(desktopQuantityInputs);
7173 removeNameAttribute(mobileQuantityInputs);
7174 } else {
7175 addNameAttribute(mobileQuantityInputs);
7176 removeNameAttribute(desktopQuantityInputs);
7177 }
7178
7179 function addNameAttribute(inputs) {
7180 inputs.forEach(function(element) {
7181 element.setAttribute('name', 'updates[]');
7182 });
7183 }
7184
7185 function removeNameAttribute(inputs) {
7186 inputs.forEach(function(element) {
7187 element.removeAttribute('name');
7188 });
7189 }
7190 },
7191
7192 _setItemLinePrice: function(item, price) {
7193 item.querySelector(selectors.cartItemLinePrice).innerHTML =
7194 price.outerHTML;
7195 },
7196
7197 _createProductDetailsList: function(
7198 product_has_only_default_variant,
7199 options,
7200 properties,
7201 selling_plan_name
7202 ) {
7203 var optionsPropertiesHTML = [];
7204
7205 if (!product_has_only_default_variant) {
7206 optionsPropertiesHTML = optionsPropertiesHTML.concat(
7207 this._getOptionList(options)
7208 );
7209 }
7210
7211 if (selling_plan_name) {
7212 optionsPropertiesHTML = optionsPropertiesHTML.concat(
7213 this._getSellingPlanName(selling_plan_name)
7214 );
7215 }
7216
7217 if (properties !== null && Object.keys(properties).length !== 0) {
7218 optionsPropertiesHTML = optionsPropertiesHTML.concat(
7219 this._getPropertyList(properties)
7220 );
7221 }
7222
7223 return optionsPropertiesHTML;
7224 },
7225
7226 _getOptionList: function(options) {
7227 return options.map(
7228 function(option) {
7229 var optionElement = this.itemOptionTemplate.cloneNode(true);
7230
7231 optionElement.textContent = option.name + ': ' + option.value;
7232 optionElement.classList.remove(classes.hide);
7233
7234 return optionElement;
7235 }.bind(this)
7236 );
7237 },
7238
7239 _getPropertyList: function(properties) {
7240 var propertiesArray =
7241 properties !== null ? Object.entries(properties) : [];
7242
7243 var filteredPropertiesArray = propertiesArray.filter(function(property) {
7244 // Line item properties prefixed with an underscore are not to be displayed
7245 // if the property value has a length of 0 (empty), don't display it
7246 if (property[0].charAt(0) === '_' || property[1].length === 0) {
7247 return false;
7248 } else {
7249 return true;
7250 }
7251 });
7252
7253 return filteredPropertiesArray.map(
7254 function(property) {
7255 var propertyElement = this.itemPropertyTemplate.cloneNode(true);
7256
7257 propertyElement.querySelector(
7258 selectors.cartItemPropertyName
7259 ).textContent = property[0] + ': ';
7260
7261 if (property[0].indexOf('/uploads/') === -1) {
7262 propertyElement.querySelector(
7263 selectors.cartItemPropertyValue
7264 ).textContent = property[1];
7265 } else {
7266 propertyElement.querySelector(
7267 selectors.cartItemPropertyValue
7268 ).innerHTML =
7269 '<a href="' +
7270 property[1] +
7271 '"> ' +
7272 property[1].split('/').pop() +
7273 '</a>';
7274 }
7275
7276 propertyElement.classList.remove(classes.hide);
7277
7278 return propertyElement;
7279 }.bind(this)
7280 );
7281 },
7282
7283 _getSellingPlanName: function(selling_plan_name) {
7284 var sellingPlanNameElement = this.itemSellingPlanNameTemplate.cloneNode(
7285 true
7286 );
7287
7288 sellingPlanNameElement.textContent = selling_plan_name;
7289 sellingPlanNameElement.classList.remove(classes.hide);
7290
7291 return sellingPlanNameElement;
7292 },
7293
7294 _createItemPrice: function(original_price, final_price) {
7295 var originalPriceHTML = theme.Currency.formatMoney(
7296 original_price,
7297 theme.moneyFormat
7298 );
7299
7300 var resultHTML;
7301
7302 if (original_price !== final_price) {
7303 resultHTML = this.itemPriceListTemplate
7304 .querySelector(selectors.cartItemDiscountedPriceGroup)
7305 .cloneNode(true);
7306
7307 resultHTML.querySelector(
7308 selectors.cartItemOriginalPrice
7309 ).innerHTML = originalPriceHTML;
7310
7311 resultHTML.querySelector(
7312 selectors.cartItemFinalPrice
7313 ).innerHTML = theme.Currency.formatMoney(
7314 final_price,
7315 theme.moneyFormat
7316 );
7317 } else {
7318 resultHTML = this.itemPriceListTemplate
7319 .querySelector(selectors.cartItemRegularPriceGroup)
7320 .cloneNode(true);
7321
7322 resultHTML.querySelector(
7323 selectors.cartItemRegularPrice
7324 ).innerHTML = originalPriceHTML;
7325 }
7326
7327 resultHTML.classList.remove(classes.hide);
7328 return resultHTML;
7329 },
7330
7331 _createUnitPrice: function(unitPrice, unitPriceMeasurement) {
7332 var unitPriceGroup = this.itemPriceListTemplate
7333 .querySelector(selectors.unitPriceGroup)
7334 .cloneNode(true);
7335
7336 var unitPriceBaseUnit =
7337 (unitPriceMeasurement.reference_value !== 1
7338 ? unitPriceMeasurement.reference_value
7339 : '') + unitPriceMeasurement.reference_unit;
7340
7341 unitPriceGroup.querySelector(
7342 selectors.unitPriceBaseUnit
7343 ).textContent = unitPriceBaseUnit;
7344
7345 unitPriceGroup.querySelector(
7346 selectors.unitPrice
7347 ).innerHTML = theme.Currency.formatMoney(unitPrice, theme.moneyFormat);
7348
7349 unitPriceGroup.classList.remove(classes.hide);
7350
7351 return unitPriceGroup;
7352 },
7353
7354 _createItemDiscountList: function(item) {
7355 return item.line_level_discount_allocations.map(
7356 function(discount) {
7357 var discountNode = this.itemDiscountTemplate.cloneNode(true);
7358
7359 discountNode.querySelector(
7360 selectors.cartItemDiscountTitle
7361 ).textContent = discount.discount_application.title;
7362
7363 discountNode.querySelector(
7364 selectors.cartItemDiscountAmount
7365 ).innerHTML = theme.Currency.formatMoney(
7366 discount.amount,
7367 theme.moneyFormat
7368 );
7369
7370 return discountNode;
7371 }.bind(this)
7372 );
7373 },
7374
7375 _showQuantityErrorMessages: function(itemElement) {
7376 itemElement
7377 .querySelectorAll(selectors.cartQuantityErrorMessage)
7378 .forEach(function(element) {
7379 element.textContent = theme.strings.quantityMinimumMessage;
7380 });
7381
7382 itemElement
7383 .querySelectorAll(selectors.cartQuantityErrorMessageWrapper)
7384 .forEach(function(element) {
7385 element.classList.remove(classes.hide);
7386 });
7387
7388 itemElement
7389 .querySelectorAll(selectors.inputQty)
7390 .forEach(function(element) {
7391 element.classList.add(classes.inputError);
7392 element.focus();
7393 });
7394 },
7395
7396 _hideQuantityErrorMessage: function() {
7397 var errorMessages = document.querySelectorAll(
7398 selectors.cartQuantityErrorMessageWrapper
7399 );
7400
7401 errorMessages.forEach(function(element) {
7402 element.classList.add(classes.hide);
7403
7404 element.querySelector(selectors.cartQuantityErrorMessage).textContent =
7405 '';
7406 });
7407
7408 this.container
7409 .querySelectorAll(selectors.inputQty)
7410 .forEach(function(element) {
7411 element.classList.remove(classes.inputError);
7412 });
7413 },
7414
7415 _handleThumbnailClick: function(evt) {
7416 if (!evt.target.classList.contains(classes.thumbnails)) return;
7417
7418 window.location.href = evt.target
7419 .closest(selectors.cartItem)
7420 .getAttribute('data-cart-item-url');
7421 },
7422
7423 _onNoteChange: function(evt) {
7424 if (!evt.target.hasAttribute('data-cart-notes')) return;
7425
7426 var note = evt.target.value;
7427 this._hideCartError();
7428 this._hideQuantityErrorMessage();
7429
7430 var headers = new Headers({ 'Content-Type': 'application/json' });
7431
7432 var request = {
7433 method: 'POST',
7434 headers: headers,
7435 body: JSON.stringify({ note: note })
7436 };
7437
7438 fetch('/cart/update.js', request).catch(
7439 function() {
7440 this._showCartError(evt.target);
7441 }.bind(this)
7442 );
7443 },
7444
7445 _showCartError: function(elementToFocus) {
7446 document.querySelector(selectors.cartErrorMessage).textContent =
7447 theme.strings.cartError;
7448
7449 document
7450 .querySelector(selectors.cartErrorMessageWrapper)
7451 .classList.remove(classes.hide);
7452
7453 if (!elementToFocus) return;
7454 elementToFocus.focus();
7455 },
7456
7457 _hideCartError: function() {
7458 document
7459 .querySelector(selectors.cartErrorMessageWrapper)
7460 .classList.add(classes.hide);
7461 document.querySelector(selectors.cartErrorMessage).textContent = '';
7462 },
7463
7464 _onRemoveItem: function(evt) {
7465 if (!evt.target.hasAttribute('data-cart-remove')) return;
7466
7467 evt.preventDefault();
7468 var lineItem = evt.target.closest(selectors.cartItem);
7469 var index = Number(lineItem.getAttribute(attributes.cartItemIndex));
7470
7471 this._hideCartError();
7472
7473 var request = {
7474 method: 'POST',
7475 headers: {
7476 'Content-Type': 'application/json;'
7477 },
7478 body: JSON.stringify({
7479 line: index,
7480 quantity: 0
7481 })
7482 };
7483
7484 fetch('/cart/change.js', request)
7485 .then(function(response) {
7486 return response.json();
7487 })
7488 .then(
7489 function(state) {
7490 if (state.item_count === 0) {
7491 this._emptyCart();
7492 } else {
7493 this._createCart(state);
7494 this._showRemoveMessage(lineItem.cloneNode(true));
7495 }
7496
7497 this._setCartCountBubble(state.item_count);
7498 }.bind(this)
7499 )
7500 .catch(
7501 function() {
7502 this._showCartError(null);
7503 }.bind(this)
7504 );
7505 },
7506
7507 _showRemoveMessage: function(lineItem) {
7508 var index = lineItem.getAttribute('data-cart-item-index');
7509 var removeMessage = this._getRemoveMessage(lineItem);
7510
7511 if (index - 1 === 0) {
7512 this.container
7513 .querySelector('[data-cart-item-index="1"]')
7514 .insertAdjacentHTML('beforebegin', removeMessage.outerHTML);
7515 } else {
7516 this.container
7517 .querySelector("[data-cart-item-index='" + (index - 1) + "']")
7518 .insertAdjacentHTML('afterend', removeMessage.outerHTML);
7519 }
7520
7521 this.container.querySelector('[data-removed-item-row]').focus();
7522 },
7523
7524 _getRemoveMessage: function(lineItem) {
7525 var formattedMessage = this._formatRemoveMessage(lineItem);
7526
7527 var tableCell = lineItem
7528 .querySelector(selectors.cartTableCell)
7529 .cloneNode(true);
7530
7531 tableCell.removeAttribute('class');
7532 tableCell.classList.add(classes.cartRemovedProduct);
7533 tableCell.setAttribute('colspan', '4');
7534 tableCell.innerHTML = formattedMessage;
7535
7536 lineItem.setAttribute('role', 'alert');
7537 lineItem.setAttribute('tabindex', '-1');
7538 lineItem.setAttribute('data-removed-item-row', true);
7539 lineItem.innerHTML = tableCell.outerHTML;
7540
7541 return lineItem;
7542 },
7543
7544 _formatRemoveMessage: function(lineItem) {
7545 var quantity = lineItem.getAttribute('data-cart-item-quantity');
7546 var url = lineItem.getAttribute(attributes.cartItemUrl);
7547 var title = lineItem.getAttribute(attributes.cartItemTitle);
7548
7549 return theme.strings.removedItemMessage
7550 .replace('[quantity]', quantity)
7551 .replace(
7552 '[link]',
7553 '<a ' +
7554 'href="' +
7555 url +
7556 '" class="text-link text-link--accent">' +
7557 title +
7558 '</a>'
7559 );
7560 },
7561
7562 _setCartCountBubble: function(quantity) {
7563 this.cartCountBubble =
7564 this.cartCountBubble ||
7565 document.querySelector(selectors.cartCountBubble);
7566
7567 this.cartCount =
7568 this.cartCount || document.querySelector(selectors.cartCount);
7569
7570 if (quantity > 0) {
7571 this.cartCountBubble.classList.remove(classes.hide);
7572 this.cartCount.textContent = quantity;
7573 } else {
7574 this.cartCountBubble.classList.add(classes.hide);
7575 this.cartCount.textContent = '';
7576 }
7577 },
7578
7579 _emptyCart: function() {
7580 this.emptyPageContent =
7581 this.emptyPageContent ||
7582 this.container.querySelector(selectors.emptyPageContent);
7583
7584 this.cartWrapper =
7585 this.cartWrapper || this.container.querySelector(selectors.cartWrapper);
7586
7587 this.emptyPageContent.classList.remove(classes.hide);
7588 this.cartWrapper.classList.add(classes.hide);
7589 }
7590 });
7591
7592 return Cart;
7593})();
7594
7595window.theme = window.theme || {};
7596
7597theme.Filters = (function() {
7598 var settings = {
7599 mediaQueryMediumUp: '(min-width: ' + theme.breakpoints.medium + 'px)'
7600 };
7601
7602 var selectors = {
7603 filterSelection: '#FilterTags',
7604 sortSelection: '#SortBy',
7605 selectInput: '[data-select-input]'
7606 };
7607
7608 function Filters(container) {
7609 this.filterSelect = container.querySelector(selectors.filterSelection);
7610 this.sortSelect = container.querySelector(selectors.sortSelection);
7611
7612 this.selects = document.querySelectorAll(selectors.selectInput);
7613
7614 if (this.sortSelect) {
7615 this.defaultSort = this._getDefaultSortValue();
7616 }
7617
7618 if (this.selects.length) {
7619 this.selects.forEach(function(select) {
7620 select.classList.remove('hidden');
7621 });
7622 }
7623
7624 this.initBreakpoints = this._initBreakpoints.bind(this);
7625
7626 this.mql = window.matchMedia(settings.mediaQueryMediumUp);
7627 this.mql.addListener(this.initBreakpoints);
7628
7629 if (this.filterSelect) {
7630 this.filterSelect.addEventListener(
7631 'change',
7632 this._onFilterChange.bind(this)
7633 );
7634 }
7635
7636 if (this.sortSelect) {
7637 this.sortSelect.addEventListener('change', this._onSortChange.bind(this));
7638 }
7639
7640 theme.Helpers.promiseStylesheet().then(
7641 function() {
7642 this._initBreakpoints();
7643 }.bind(this)
7644 );
7645 this._initParams();
7646 }
7647
7648 Filters.prototype = Object.assign({}, Filters.prototype, {
7649 _initBreakpoints: function() {
7650 if (this.mql.matches) {
7651 slate.utils.resizeSelects(this.selects);
7652 }
7653 },
7654
7655 _initParams: function() {
7656 this.queryParams = {};
7657 if (location.search.length) {
7658 var aKeyValue;
7659 var aCouples = location.search.substr(1).split('&');
7660 for (var i = 0; i < aCouples.length; i++) {
7661 aKeyValue = aCouples[i].split('=');
7662 if (aKeyValue.length > 1) {
7663 this.queryParams[
7664 decodeURIComponent(aKeyValue[0])
7665 ] = decodeURIComponent(aKeyValue[1]);
7666 }
7667 }
7668 }
7669 },
7670
7671 _onSortChange: function() {
7672 this.queryParams.sort_by = this._getSortValue();
7673
7674 if (this.queryParams.page) {
7675 delete this.queryParams.page;
7676 }
7677
7678 window.location.search = decodeURIComponent(
7679 new URLSearchParams(Object.entries(this.queryParams)).toString()
7680 );
7681 },
7682
7683 _onFilterChange: function() {
7684 document.location.href = this._getFilterValue();
7685 },
7686
7687 _getFilterValue: function() {
7688 return this.filterSelect.value;
7689 },
7690
7691 _getSortValue: function() {
7692 return this.sortSelect.value || this.defaultSort;
7693 },
7694
7695 _getDefaultSortValue: function() {
7696 return this.sortSelect.dataset.defaultSortby;
7697 },
7698
7699 onUnload: function() {
7700 if (this.filterSelect) {
7701 this.filterSelect.removeEventListener('change', this._onFilterChange);
7702 }
7703
7704 if (this.sortSelect) {
7705 this.sortSelect.removeEventListener('change', this._onSortChange);
7706 }
7707
7708 this.mql.removeListener(this.initBreakpoints);
7709 }
7710 });
7711
7712 return Filters;
7713})();
7714
7715window.theme = window.theme || {};
7716
7717theme.HeaderSection = (function() {
7718 function Header() {
7719 theme.Header.init();
7720 theme.MobileNav.init();
7721 theme.SearchDrawer.init();
7722 theme.Search.init();
7723 }
7724
7725 Header.prototype = Object.assign({}, Header.prototype, {
7726 onUnload: function() {
7727 theme.Header.unload();
7728 theme.Search.unload();
7729 theme.MobileNav.unload();
7730 }
7731 });
7732
7733 return Header;
7734})();
7735
7736theme.Maps = (function() {
7737 var config = {
7738 zoom: 14
7739 };
7740 var apiStatus = null;
7741 var mapsToLoad = [];
7742
7743 var errors = {
7744 addressNoResults: theme.strings.addressNoResults,
7745 addressQueryLimit: theme.strings.addressQueryLimit,
7746 addressError: theme.strings.addressError,
7747 authError: theme.strings.authError
7748 };
7749
7750 var selectors = {
7751 section: '[data-section-type="map"]',
7752 map: '[data-map]',
7753 mapOverlay: '[data-map-overlay]'
7754 };
7755
7756 var classes = {
7757 mapError: 'map-section--load-error',
7758 errorMsg: 'map-section__error errors text-center'
7759 };
7760
7761 // Global function called by Google on auth errors.
7762 // Show an auto error message on all map instances.
7763 // eslint-disable-next-line camelcase, no-unused-vars
7764 window.gm_authFailure = function() {
7765 if (!Shopify.designMode) {
7766 return;
7767 }
7768
7769 document.querySelector(selectors.section).classList.add(classes.mapError);
7770 document.querySelector(selectors.map).remove();
7771 document
7772 .querySelector(selectors.mapOverlay)
7773 .insertAdjacentHTML(
7774 'afterend',
7775 '<div class="' +
7776 classes.errorMsg +
7777 '">' +
7778 theme.strings.authError +
7779 '</div>'
7780 );
7781 };
7782
7783 function Map(container) {
7784 this.map = container.querySelector(selectors.map);
7785 if (!this.map) return;
7786 this.key = this.map.dataset.apiKey;
7787
7788 if (typeof this.key === 'undefined') {
7789 return;
7790 }
7791
7792 if (apiStatus === 'loaded') {
7793 this.createMap();
7794 } else {
7795 mapsToLoad.push(this);
7796
7797 if (apiStatus !== 'loading') {
7798 apiStatus = 'loading';
7799 if (typeof window.google === 'undefined') {
7800 theme.Helpers.getScript(
7801 'https://maps.googleapis.com/maps/api/js?key=' + this.key
7802 ).then(function() {
7803 apiStatus = 'loaded';
7804 initAllMaps();
7805 });
7806 }
7807 }
7808 }
7809 }
7810
7811 function initAllMaps() {
7812 // API has loaded, load all Map instances in queue
7813 mapsToLoad.forEach(function(map) {
7814 map.createMap();
7815 });
7816 }
7817
7818 function geolocate(map) {
7819 return new Promise(function(resolve, reject) {
7820 var geocoder = new google.maps.Geocoder();
7821 var address = map.dataset.addressSetting;
7822
7823 geocoder.geocode({ address: address }, function(results, status) {
7824 if (status !== google.maps.GeocoderStatus.OK) {
7825 reject(status);
7826 }
7827
7828 resolve(results);
7829 });
7830 });
7831 }
7832
7833 Map.prototype = Object.assign({}, Map.prototype, {
7834 createMap: function() {
7835 return geolocate(this.map)
7836 .then(
7837 function(results) {
7838 var mapOptions = {
7839 zoom: config.zoom,
7840 center: results[0].geometry.location,
7841 draggable: false,
7842 clickableIcons: false,
7843 scrollwheel: false,
7844 disableDoubleClickZoom: true,
7845 disableDefaultUI: true
7846 };
7847
7848 var map = (this.map = new google.maps.Map(this.map, mapOptions));
7849 var center = (this.center = map.getCenter());
7850
7851 //eslint-disable-next-line no-unused-vars
7852 var marker = new google.maps.Marker({
7853 map: map,
7854 position: map.getCenter()
7855 });
7856
7857 google.maps.event.addDomListener(
7858 window,
7859 'resize',
7860 theme.Helpers.debounce(
7861 function() {
7862 google.maps.event.trigger(map, 'resize');
7863 map.setCenter(center);
7864 this.map.removeAttribute('style');
7865 }.bind(this),
7866 250
7867 )
7868 );
7869 }.bind(this)
7870 )
7871 .catch(
7872 function() {
7873 var errorMessage;
7874
7875 switch (status) {
7876 case 'ZERO_RESULTS':
7877 errorMessage = errors.addressNoResults;
7878 break;
7879 case 'OVER_QUERY_LIMIT':
7880 errorMessage = errors.addressQueryLimit;
7881 break;
7882 case 'REQUEST_DENIED':
7883 errorMessage = errors.authError;
7884 break;
7885 default:
7886 errorMessage = errors.addressError;
7887 break;
7888 }
7889
7890 // Show errors only to merchant in the editor.
7891 if (Shopify.designMode) {
7892 this.map.parentNode.classList.add(classes.mapError);
7893 this.map.parentNode.innerHTML =
7894 '<div class="' +
7895 classes.errorMsg +
7896 '">' +
7897 errorMessage +
7898 '</div>';
7899 }
7900 }.bind(this)
7901 );
7902 },
7903
7904 onUnload: function() {
7905 if (this.map) {
7906 google.maps.event.clearListeners(this.map, 'resize');
7907 }
7908 }
7909 });
7910
7911 return Map;
7912})();
7913
7914/* eslint-disable no-new */
7915theme.Product = (function() {
7916 function Product(container) {
7917 this.container = container;
7918 var sectionId = container.getAttribute('data-section-id');
7919 this.zoomPictures = [];
7920 this.ajaxEnabled = container.getAttribute('data-ajax-enabled') === 'true';
7921
7922 this.settings = {
7923 // Breakpoints from src/stylesheets/global/variables.scss.liquid
7924 mediaQueryMediumUp: 'screen and (min-width: 750px)',
7925 mediaQuerySmall: 'screen and (max-width: 749px)',
7926 bpSmall: false,
7927 enableHistoryState:
7928 container.getAttribute('data-enable-history-state') || false,
7929 namespace: '.slideshow-' + sectionId,
7930 sectionId: sectionId,
7931 sliderActive: false,
7932 zoomEnabled: false
7933 };
7934
7935 this.selectors = {
7936 addToCart: '[data-add-to-cart]',
7937 addToCartText: '[data-add-to-cart-text]',
7938 cartCount: '[data-cart-count]',
7939 cartCountBubble: '[data-cart-count-bubble]',
7940 cartPopup: '[data-cart-popup]',
7941 cartPopupCartQuantity: '[data-cart-popup-cart-quantity]',
7942 cartPopupClose: '[data-cart-popup-close]',
7943 cartPopupDismiss: '[data-cart-popup-dismiss]',
7944 cartPopupImage: '[data-cart-popup-image]',
7945 cartPopupImageWrapper: '[data-cart-popup-image-wrapper]',
7946 cartPopupImagePlaceholder: '[data-image-loading-animation]',
7947 cartPopupProductDetails: '[data-cart-popup-product-details]',
7948 cartPopupQuantity: '[data-cart-popup-quantity]',
7949 cartPopupQuantityLabel: '[data-cart-popup-quantity-label]',
7950 cartPopupTitle: '[data-cart-popup-title]',
7951 cartPopupWrapper: '[data-cart-popup-wrapper]',
7952 loader: '[data-loader]',
7953 loaderStatus: '[data-loader-status]',
7954 quantity: '[data-quantity-input]',
7955 SKU: '.variant-sku',
7956 productStatus: '[data-product-status]',
7957 originalSelectorId: '#ProductSelect-' + sectionId,
7958 productForm: '[data-product-form]',
7959 errorMessage: '[data-error-message]',
7960 errorMessageWrapper: '[data-error-message-wrapper]',
7961 imageZoomWrapper: '[data-image-zoom-wrapper]',
7962 productMediaWrapper: '[data-product-single-media-wrapper]',
7963 productThumbImages: '.product-single__thumbnail--' + sectionId,
7964 productThumbs: '.product-single__thumbnails-' + sectionId,
7965 productThumbListItem: '.product-single__thumbnails-item',
7966 productThumbsWrapper: '.thumbnails-wrapper',
7967 saleLabel: '.product-price__sale-label-' + sectionId,
7968 singleOptionSelector: '.single-option-selector-' + sectionId,
7969 shopifyPaymentButton: '.shopify-payment-button',
7970 productMediaTypeVideo: '[data-product-media-type-video]',
7971 productMediaTypeModel: '[data-product-media-type-model]',
7972 priceContainer: '[data-price]',
7973 regularPrice: '[data-regular-price]',
7974 salePrice: '[data-sale-price]',
7975 unitPrice: '[data-unit-price]',
7976 unitPriceBaseUnit: '[data-unit-price-base-unit]',
7977 productPolicies: '[data-product-policies]',
7978 storeAvailabilityContainer: '[data-store-availability-container]'
7979 };
7980
7981 this.classes = {
7982 cartPopupWrapperHidden: 'cart-popup-wrapper--hidden',
7983 hidden: 'hide',
7984 visibilityHidden: 'visibility-hidden',
7985 inputError: 'input--error',
7986 jsZoomEnabled: 'js-zoom-enabled',
7987 productOnSale: 'price--on-sale',
7988 productUnitAvailable: 'price--unit-available',
7989 productUnavailable: 'price--unavailable',
7990 productSoldOut: 'price--sold-out',
7991 cartImage: 'cart-popup-item__image',
7992 productFormErrorMessageWrapperHidden:
7993 'product-form__error-message-wrapper--hidden',
7994 activeClass: 'active-thumb',
7995 variantSoldOut: 'product-form--variant-sold-out'
7996 };
7997
7998 this.eventHandlers = {};
7999
8000 this.quantityInput = container.querySelector(this.selectors.quantity);
8001 this.errorMessageWrapper = container.querySelector(
8002 this.selectors.errorMessageWrapper
8003 );
8004 this.productForm = container.querySelector(this.selectors.productForm);
8005 this.addToCart = container.querySelector(this.selectors.addToCart);
8006 this.addToCartText = this.addToCart.querySelector(
8007 this.selectors.addToCartText
8008 );
8009 this.shopifyPaymentButton = container.querySelector(
8010 this.selectors.shopifyPaymentButton
8011 );
8012 this.productPolicies = container.querySelector(
8013 this.selectors.productPolicies
8014 );
8015 this.storeAvailabilityContainer = container.querySelector(
8016 this.selectors.storeAvailabilityContainer
8017 );
8018 if (this.storeAvailabilityContainer) {
8019 this._initStoreAvailability();
8020 }
8021
8022 this.loader = this.addToCart.querySelector(this.selectors.loader);
8023 this.loaderStatus = container.querySelector(this.selectors.loaderStatus);
8024
8025 this.imageZoomWrapper = container.querySelectorAll(
8026 this.selectors.imageZoomWrapper
8027 );
8028
8029 // Stop parsing if we don't have the product json script tag when loading
8030 // section in the Theme Editor
8031 var productJson = document.getElementById('ProductJson-' + sectionId);
8032 if (!productJson || !productJson.innerHTML.length) {
8033 return;
8034 }
8035
8036 this.productSingleObject = JSON.parse(productJson.innerHTML);
8037
8038 // Initial state for global productState object
8039 this.productState = {
8040 available: true,
8041 soldOut: false,
8042 onSale: false,
8043 showUnitPrice: false
8044 };
8045
8046 this.settings.zoomEnabled =
8047 this.imageZoomWrapper.length > 0
8048 ? this.imageZoomWrapper[0].classList.contains(
8049 this.classes.jsZoomEnabled
8050 )
8051 : false;
8052
8053 this.initMobileBreakpoint = this._initMobileBreakpoint.bind(this);
8054 this.initDesktopBreakpoint = this._initDesktopBreakpoint.bind(this);
8055
8056 this.mqlSmall = window.matchMedia(this.settings.mediaQuerySmall);
8057 this.mqlSmall.addListener(this.initMobileBreakpoint);
8058
8059 this.mqlMediumUp = window.matchMedia(this.settings.mediaQueryMediumUp);
8060 this.mqlMediumUp.addListener(this.initDesktopBreakpoint);
8061
8062 this.initMobileBreakpoint();
8063 this.initDesktopBreakpoint();
8064 this._stringOverrides();
8065 this._initVariants();
8066 this._initMediaSwitch();
8067 this._initAddToCart();
8068 this._setActiveThumbnail();
8069 this._initProductVideo();
8070 this._initModelViewerLibraries();
8071 this._initShopifyXrLaunch();
8072 }
8073
8074 Product.prototype = Object.assign({}, Product.prototype, {
8075 _stringOverrides: function() {
8076 theme.productStrings = theme.productStrings || {};
8077 theme.strings = Object.assign({}, theme.strings, theme.productStrings);
8078 },
8079
8080 _initStoreAvailability: function() {
8081 this.storeAvailability = new theme.StoreAvailability(
8082 this.storeAvailabilityContainer
8083 );
8084
8085 var storeAvailabilityModalOpenedCallback = function(event) {
8086 if (
8087 this.cartPopupWrapper &&
8088 !this.cartPopupWrapper.classList.contains(
8089 this.classes.cartPopupWrapperHidden
8090 )
8091 ) {
8092 this._hideCartPopup(event);
8093 }
8094 };
8095
8096 // hide cart popup modal if the store availability modal is also opened
8097 this.storeAvailabilityContainer.addEventListener(
8098 'storeAvailabilityModalOpened',
8099 storeAvailabilityModalOpenedCallback.bind(this)
8100 );
8101 },
8102
8103 _initMobileBreakpoint: function() {
8104 if (this.mqlSmall.matches) {
8105 // initialize thumbnail slider on mobile if more than four thumbnails
8106 if (
8107 this.container.querySelectorAll(this.selectors.productThumbImages)
8108 .length > 4
8109 ) {
8110 this._initThumbnailSlider();
8111 }
8112
8113 // destroy image zooming if enabled
8114 if (this.settings.zoomEnabled) {
8115 this.imageZoomWrapper.forEach(
8116 function(element, index) {
8117 this._destroyZoom(index);
8118 }.bind(this)
8119 );
8120 }
8121
8122 this.settings.bpSmall = true;
8123 } else {
8124 if (this.settings.sliderActive) {
8125 this._destroyThumbnailSlider();
8126 }
8127
8128 this.settings.bpSmall = false;
8129 }
8130 },
8131
8132 _initDesktopBreakpoint: function() {
8133 if (this.mqlMediumUp.matches && this.settings.zoomEnabled) {
8134 this.imageZoomWrapper.forEach(
8135 function(element, index) {
8136 this._enableZoom(element, index);
8137 }.bind(this)
8138 );
8139 }
8140 },
8141
8142 _initVariants: function() {
8143 var options = {
8144 container: this.container,
8145 enableHistoryState:
8146 this.container.getAttribute('data-enable-history-state') || false,
8147 singleOptionSelector: this.selectors.singleOptionSelector,
8148 originalSelectorId: this.selectors.originalSelectorId,
8149 product: this.productSingleObject
8150 };
8151
8152 this.variants = new slate.Variants(options);
8153 if (this.storeAvailability && this.variants.currentVariant.available) {
8154 this.storeAvailability.updateContent(this.variants.currentVariant.id);
8155 }
8156
8157 this.eventHandlers.updateAvailability = this._updateAvailability.bind(
8158 this
8159 );
8160 this.eventHandlers.updateMedia = this._updateMedia.bind(this);
8161 this.eventHandlers.updatePrice = this._updatePrice.bind(this);
8162 this.eventHandlers.updateSKU = this._updateSKU.bind(this);
8163
8164 this.container.addEventListener(
8165 'variantChange',
8166 this.eventHandlers.updateAvailability
8167 );
8168 this.container.addEventListener(
8169 'variantImageChange',
8170 this.eventHandlers.updateMedia
8171 );
8172 this.container.addEventListener(
8173 'variantPriceChange',
8174 this.eventHandlers.updatePrice
8175 );
8176 this.container.addEventListener(
8177 'variantSKUChange',
8178 this.eventHandlers.updateSKU
8179 );
8180 },
8181
8182 _initMediaSwitch: function() {
8183 if (!document.querySelector(this.selectors.productThumbImages)) {
8184 return;
8185 }
8186
8187 var self = this;
8188
8189 var productThumbImages = document.querySelectorAll(
8190 this.selectors.productThumbImages
8191 );
8192
8193 this.eventHandlers.handleMediaFocus = this._handleMediaFocus.bind(this);
8194
8195 productThumbImages.forEach(function(el) {
8196 el.addEventListener('click', function(evt) {
8197 evt.preventDefault();
8198 var mediaId = el.getAttribute('data-thumbnail-id');
8199
8200 self._switchMedia(mediaId);
8201 self._setActiveThumbnail(mediaId);
8202 });
8203 el.addEventListener('keyup', self.eventHandlers.handleMediaFocus);
8204 });
8205 },
8206
8207 _initAddToCart: function() {
8208 this.productForm.addEventListener(
8209 'submit',
8210 function(evt) {
8211 if (this.addToCart.getAttribute('aria-disabled') === 'true') {
8212 evt.preventDefault();
8213 return;
8214 }
8215
8216 if (!this.ajaxEnabled) return;
8217
8218 evt.preventDefault();
8219
8220 this.previouslyFocusedElement = document.activeElement;
8221
8222 var isInvalidQuantity =
8223 !!this.quantityInput && this.quantityInput.value <= 0;
8224
8225 if (isInvalidQuantity) {
8226 this._showErrorMessage(theme.strings.quantityMinimumMessage);
8227 return;
8228 }
8229
8230 if (!isInvalidQuantity && this.ajaxEnabled) {
8231 // disable the addToCart and dynamic checkout button while
8232 // request/cart popup is loading and handle loading state
8233 this._handleButtonLoadingState(true);
8234 this._addItemToCart(this.productForm);
8235 return;
8236 }
8237 }.bind(this)
8238 );
8239 },
8240
8241 _initProductVideo: function() {
8242 var sectionId = this.settings.sectionId;
8243
8244 var productMediaTypeVideo = this.container.querySelectorAll(
8245 this.selectors.productMediaTypeVideo
8246 );
8247 productMediaTypeVideo.forEach(function(el) {
8248 theme.ProductVideo.init(el, sectionId);
8249 });
8250 },
8251
8252 _initModelViewerLibraries: function() {
8253 var modelViewerElements = this.container.querySelectorAll(
8254 this.selectors.productMediaTypeModel
8255 );
8256 if (modelViewerElements.length < 1) return;
8257 theme.ProductModel.init(modelViewerElements, this.settings.sectionId);
8258 },
8259
8260 _initShopifyXrLaunch: function() {
8261 this.eventHandlers.initShopifyXrLaunchHandler = this._initShopifyXrLaunchHandler.bind(
8262 this
8263 );
8264 document.addEventListener(
8265 'shopify_xr_launch',
8266 this.eventHandlers.initShopifyXrLaunchHandler
8267 );
8268 },
8269
8270 _initShopifyXrLaunchHandler: function() {
8271 var currentMedia = this.container.querySelector(
8272 this.selectors.productMediaWrapper +
8273 ':not(.' +
8274 self.classes.hidden +
8275 ')'
8276 );
8277 currentMedia.dispatchEvent(
8278 new CustomEvent('xrLaunch', {
8279 bubbles: true,
8280 cancelable: true
8281 })
8282 );
8283 },
8284
8285 _addItemToCart: function(form) {
8286 var self = this;
8287
8288 fetch('/cart/add.js', {
8289 method: 'POST',
8290 credentials: 'same-origin',
8291 headers: {
8292 'Content-Type': 'application/x-www-form-urlencoded',
8293 'X-Requested-With': 'XMLHttpRequest'
8294 },
8295 body: theme.Helpers.serialize(form)
8296 })
8297 .then(function(response) {
8298 return response.json();
8299 })
8300 .then(function(json) {
8301 if (json.status && json.status !== 200) {
8302 var error = new Error(json.description);
8303 error.isFromServer = true;
8304 throw error;
8305 }
8306 self._hideErrorMessage();
8307 self._setupCartPopup(json);
8308 })
8309 .catch(function(error) {
8310 self.previouslyFocusedElement.focus();
8311 self._showErrorMessage(
8312 error.isFromServer && error.message.length
8313 ? error.message
8314 : theme.strings.cartError
8315 );
8316 self._handleButtonLoadingState(false);
8317 // eslint-disable-next-line no-console
8318 console.log(error);
8319 });
8320 },
8321
8322 _handleButtonLoadingState: function(isLoading) {
8323 if (isLoading) {
8324 this.addToCart.setAttribute('aria-disabled', true);
8325 this.addToCartText.classList.add(this.classes.hidden);
8326 this.loader.classList.remove(this.classes.hidden);
8327
8328 if (this.shopifyPaymentButton) {
8329 this.shopifyPaymentButton.setAttribute('disabled', true);
8330 }
8331
8332 this.loaderStatus.setAttribute('aria-hidden', false);
8333 } else {
8334 this.addToCart.removeAttribute('aria-disabled');
8335 this.addToCartText.classList.remove(this.classes.hidden);
8336 this.loader.classList.add(this.classes.hidden);
8337
8338 if (this.shopifyPaymentButton) {
8339 this.shopifyPaymentButton.removeAttribute('disabled');
8340 }
8341
8342 this.loaderStatus.setAttribute('aria-hidden', true);
8343 }
8344 },
8345
8346 _showErrorMessage: function(errorMessage) {
8347 var errorMessageContainer = this.container.querySelector(
8348 this.selectors.errorMessage
8349 );
8350 errorMessageContainer.innerHTML = errorMessage;
8351
8352 if (this.quantityInput) {
8353 this.quantityInput.classList.add(this.classes.inputError);
8354 }
8355
8356 this.errorMessageWrapper.classList.remove(
8357 this.classes.productFormErrorMessageWrapperHidden
8358 );
8359 this.errorMessageWrapper.setAttribute('aria-hidden', true);
8360 this.errorMessageWrapper.removeAttribute('aria-hidden');
8361 },
8362
8363 _hideErrorMessage: function() {
8364 this.errorMessageWrapper.classList.add(
8365 this.classes.productFormErrorMessageWrapperHidden
8366 );
8367
8368 if (this.quantityInput) {
8369 this.quantityInput.classList.remove(this.classes.inputError);
8370 }
8371 },
8372
8373 _setupCartPopup: function(item) {
8374 this.cartPopup =
8375 this.cartPopup || document.querySelector(this.selectors.cartPopup);
8376 this.cartPopupWrapper =
8377 this.cartPopupWrapper ||
8378 document.querySelector(this.selectors.cartPopupWrapper);
8379 this.cartPopupTitle =
8380 this.cartPopupTitle ||
8381 document.querySelector(this.selectors.cartPopupTitle);
8382 this.cartPopupQuantity =
8383 this.cartPopupQuantity ||
8384 document.querySelector(this.selectors.cartPopupQuantity);
8385 this.cartPopupQuantityLabel =
8386 this.cartPopupQuantityLabel ||
8387 document.querySelector(this.selectors.cartPopupQuantityLabel);
8388 this.cartPopupClose =
8389 this.cartPopupClose ||
8390 document.querySelector(this.selectors.cartPopupClose);
8391 this.cartPopupDismiss =
8392 this.cartPopupDismiss ||
8393 document.querySelector(this.selectors.cartPopupDismiss);
8394 this.cartPopupImagePlaceholder =
8395 this.cartPopupImagePlaceholder ||
8396 document.querySelector(this.selectors.cartPopupImagePlaceholder);
8397
8398 this._setupCartPopupEventListeners();
8399
8400 this._updateCartPopupContent(item);
8401 },
8402
8403 _updateCartPopupContent: function(item) {
8404 var self = this;
8405
8406 var quantity = this.quantityInput ? this.quantityInput.value : 1;
8407
8408 var selling_plan_name = item.selling_plan_allocation
8409 ? item.selling_plan_allocation.selling_plan.name
8410 : null;
8411
8412 this.cartPopupTitle.textContent = item.product_title;
8413 this.cartPopupQuantity.textContent = quantity;
8414 this.cartPopupQuantityLabel.textContent = theme.strings.quantityLabel.replace(
8415 '[count]',
8416 quantity
8417 );
8418
8419 this._setCartPopupPlaceholder(item.featured_image.url);
8420 this._setCartPopupImage(item.featured_image.url, item.featured_image.alt);
8421 this._setCartPopupProductDetails(
8422 item.product_has_only_default_variant,
8423 item.options_with_values,
8424 item.properties,
8425 selling_plan_name
8426 );
8427
8428 fetch('/cart.js', { credentials: 'same-origin' })
8429 .then(function(response) {
8430 return response.json();
8431 })
8432 .then(function(cart) {
8433 self._setCartQuantity(cart.item_count);
8434 self._setCartCountBubble(cart.item_count);
8435 self._showCartPopup();
8436 })
8437 .catch(function(error) {
8438 // eslint-disable-next-line no-console
8439 console.log(error);
8440 });
8441 },
8442
8443 _setupCartPopupEventListeners: function() {
8444 this.eventHandlers.cartPopupWrapperKeyupHandler = this._cartPopupWrapperKeyupHandler.bind(
8445 this
8446 );
8447 this.eventHandlers.hideCartPopup = this._hideCartPopup.bind(this);
8448 this.eventHandlers.onBodyClick = this._onBodyClick.bind(this);
8449
8450 this.cartPopupWrapper.addEventListener(
8451 'keyup',
8452 this.eventHandlers.cartPopupWrapperKeyupHandler
8453 );
8454 this.cartPopupClose.addEventListener(
8455 'click',
8456 this.eventHandlers.hideCartPopup
8457 );
8458 this.cartPopupDismiss.addEventListener(
8459 'click',
8460 this.eventHandlers.hideCartPopup
8461 );
8462 document.body.addEventListener('click', this.eventHandlers.onBodyClick);
8463 },
8464
8465 _cartPopupWrapperKeyupHandler: function(event) {
8466 if (event.keyCode === slate.utils.keyboardKeys.ESCAPE) {
8467 this._hideCartPopup(event);
8468 }
8469 },
8470
8471 _setCartPopupPlaceholder: function(imageUrl) {
8472 this.cartPopupImageWrapper =
8473 this.cartPopupImageWrapper ||
8474 document.querySelector(this.selectors.cartPopupImageWrapper);
8475
8476 if (imageUrl === null) {
8477 this.cartPopupImageWrapper.classList.add(this.classes.hidden);
8478 return;
8479 }
8480 },
8481
8482 _setCartPopupImage: function(imageUrl, imageAlt) {
8483 if (imageUrl === null) return;
8484
8485 this.cartPopupImageWrapper.classList.remove(this.classes.hidden);
8486 var sizedImageUrl = theme.Images.getSizedImageUrl(imageUrl, '200x');
8487 var image = document.createElement('img');
8488 image.src = sizedImageUrl;
8489 image.alt = imageAlt;
8490 image.classList.add(this.classes.cartImage);
8491 image.setAttribute('data-cart-popup-image', '');
8492
8493 image.onload = function() {
8494 this.cartPopupImagePlaceholder.removeAttribute(
8495 'data-image-loading-animation'
8496 );
8497 this.cartPopupImageWrapper.append(image);
8498 }.bind(this);
8499 },
8500
8501 _setCartPopupProductDetails: function(
8502 product_has_only_default_variant,
8503 options,
8504 properties,
8505 selling_plan_name
8506 ) {
8507 this.cartPopupProductDetails =
8508 this.cartPopupProductDetails ||
8509 document.querySelector(this.selectors.cartPopupProductDetails);
8510 var variantPropertiesHTML = '';
8511
8512 if (!product_has_only_default_variant) {
8513 variantPropertiesHTML =
8514 variantPropertiesHTML + this._getVariantOptionList(options);
8515 }
8516
8517 if (selling_plan_name) {
8518 variantPropertiesHTML =
8519 variantPropertiesHTML + this._getSellingPlanHTML(selling_plan_name);
8520 }
8521
8522 if (properties !== null && Object.keys(properties).length !== 0) {
8523 variantPropertiesHTML =
8524 variantPropertiesHTML + this._getPropertyList(properties);
8525 }
8526
8527 if (variantPropertiesHTML.length === 0) {
8528 this.cartPopupProductDetails.innerHTML = '';
8529 this.cartPopupProductDetails.setAttribute('hidden', '');
8530 } else {
8531 this.cartPopupProductDetails.innerHTML = variantPropertiesHTML;
8532 this.cartPopupProductDetails.removeAttribute('hidden');
8533 }
8534 },
8535
8536 _getVariantOptionList: function(variantOptions) {
8537 var variantOptionListHTML = '';
8538
8539 variantOptions.forEach(function(variantOption) {
8540 variantOptionListHTML =
8541 variantOptionListHTML +
8542 '<li class="product-details__item product-details__item--variant-option">' +
8543 variantOption.name +
8544 ': ' +
8545 variantOption.value +
8546 '</li>';
8547 });
8548
8549 return variantOptionListHTML;
8550 },
8551
8552 _getPropertyList: function(properties) {
8553 var propertyListHTML = '';
8554 var propertiesArray = Object.entries(properties);
8555
8556 propertiesArray.forEach(function(property) {
8557 // Line item properties prefixed with an underscore are not to be displayed
8558 if (property[0].charAt(0) === '_') return;
8559
8560 // if the property value has a length of 0 (empty), don't display it
8561 if (property[1].length === 0) return;
8562
8563 propertyListHTML =
8564 propertyListHTML +
8565 '<li class="product-details__item product-details__item--property">' +
8566 '<span class="product-details__property-label">' +
8567 property[0] +
8568 ': </span>' +
8569 property[1];
8570 ': ' + '</li>';
8571 });
8572
8573 return propertyListHTML;
8574 },
8575
8576 _getSellingPlanHTML: function(selling_plan_name) {
8577 var sellingPlanHTML =
8578 '<li class="product-details__item product-details__item--property">' +
8579 selling_plan_name +
8580 '</li>';
8581
8582 return sellingPlanHTML;
8583 },
8584
8585 _setCartQuantity: function(quantity) {
8586 this.cartPopupCartQuantity =
8587 this.cartPopupCartQuantity ||
8588 document.querySelector(this.selectors.cartPopupCartQuantity);
8589 var ariaLabel;
8590
8591 if (quantity === 1) {
8592 ariaLabel = theme.strings.oneCartCount;
8593 } else if (quantity > 1) {
8594 ariaLabel = theme.strings.otherCartCount.replace('[count]', quantity);
8595 }
8596
8597 this.cartPopupCartQuantity.textContent = quantity;
8598 this.cartPopupCartQuantity.setAttribute('aria-label', ariaLabel);
8599 },
8600
8601 _setCartCountBubble: function(quantity) {
8602 this.cartCountBubble =
8603 this.cartCountBubble ||
8604 document.querySelector(this.selectors.cartCountBubble);
8605 this.cartCount =
8606 this.cartCount || document.querySelector(this.selectors.cartCount);
8607
8608 this.cartCountBubble.classList.remove(this.classes.hidden);
8609 this.cartCount.textContent = quantity;
8610 },
8611
8612 _showCartPopup: function() {
8613 theme.Helpers.prepareTransition(this.cartPopupWrapper);
8614
8615 this.cartPopupWrapper.classList.remove(
8616 this.classes.cartPopupWrapperHidden
8617 );
8618 this._handleButtonLoadingState(false);
8619
8620 slate.a11y.trapFocus({
8621 container: this.cartPopupWrapper,
8622 elementToFocus: this.cartPopup,
8623 namespace: 'cartPopupFocus'
8624 });
8625 },
8626
8627 _hideCartPopup: function(event) {
8628 var setFocus = event.detail === 0 ? true : false;
8629 theme.Helpers.prepareTransition(this.cartPopupWrapper);
8630 this.cartPopupWrapper.classList.add(this.classes.cartPopupWrapperHidden);
8631
8632 var cartPopupImage = document.querySelector(
8633 this.selectors.cartPopupImage
8634 );
8635 if (cartPopupImage) {
8636 cartPopupImage.remove();
8637 }
8638 this.cartPopupImagePlaceholder.setAttribute(
8639 'data-image-loading-animation',
8640 ''
8641 );
8642
8643 slate.a11y.removeTrapFocus({
8644 container: this.cartPopupWrapper,
8645 namespace: 'cartPopupFocus'
8646 });
8647
8648 if (setFocus) this.previouslyFocusedElement.focus();
8649
8650 this.cartPopupWrapper.removeEventListener(
8651 'keyup',
8652 this.eventHandlers.cartPopupWrapperKeyupHandler
8653 );
8654 this.cartPopupClose.removeEventListener(
8655 'click',
8656 this.eventHandlers.hideCartPopup
8657 );
8658 this.cartPopupDismiss.removeEventListener(
8659 'click',
8660 this.eventHandlers.hideCartPopup
8661 );
8662 document.body.removeEventListener(
8663 'click',
8664 this.eventHandlers.onBodyClick
8665 );
8666 },
8667
8668 _onBodyClick: function(event) {
8669 var target = event.target;
8670
8671 if (
8672 target !== this.cartPopupWrapper &&
8673 !target.closest(this.selectors.cartPopup)
8674 ) {
8675 this._hideCartPopup(event);
8676 }
8677 },
8678
8679 _setActiveThumbnail: function(mediaId) {
8680 // If there is no element passed, find it by the current product image
8681 if (typeof mediaId === 'undefined') {
8682 var productMediaWrapper = this.container.querySelector(
8683 this.selectors.productMediaWrapper + ':not(.hide)'
8684 );
8685
8686 if (!productMediaWrapper) return;
8687 mediaId = productMediaWrapper.getAttribute('data-media-id');
8688 }
8689
8690 var thumbnailWrappers = this.container.querySelectorAll(
8691 this.selectors.productThumbListItem + ':not(.slick-cloned)'
8692 );
8693
8694 var activeThumbnail;
8695 thumbnailWrappers.forEach(
8696 function(el) {
8697 var current = el.querySelector(
8698 this.selectors.productThumbImages +
8699 "[data-thumbnail-id='" +
8700 mediaId +
8701 "']"
8702 );
8703 if (current) {
8704 activeThumbnail = current;
8705 }
8706 }.bind(this)
8707 );
8708
8709 var productThumbImages = document.querySelectorAll(
8710 this.selectors.productThumbImages
8711 );
8712 productThumbImages.forEach(
8713 function(el) {
8714 el.classList.remove(this.classes.activeClass);
8715 el.removeAttribute('aria-current');
8716 }.bind(this)
8717 );
8718
8719 if (activeThumbnail) {
8720 activeThumbnail.classList.add(this.classes.activeClass);
8721 activeThumbnail.setAttribute('aria-current', true);
8722 this._adjustThumbnailSlider(activeThumbnail);
8723 }
8724 },
8725
8726 _adjustThumbnailSlider: function(activeThumbnail) {
8727 var sliderItem = activeThumbnail.closest('[data-slider-item]');
8728 if (!sliderItem) return;
8729
8730 var slideGroupLeaderIndex =
8731 Math.floor(
8732 Number(sliderItem.getAttribute('data-slider-slide-index')) / 3
8733 ) * 3;
8734
8735 window.setTimeout(
8736 function() {
8737 if (!this.slideshow) return;
8738 this.slideshow.goToSlideByIndex(slideGroupLeaderIndex);
8739 }.bind(this),
8740 251
8741 );
8742 },
8743
8744 _switchMedia: function(mediaId) {
8745 var currentMedia = this.container.querySelector(
8746 this.selectors.productMediaWrapper +
8747 ':not(.' +
8748 this.classes.hidden +
8749 ')'
8750 );
8751
8752 var newMedia = this.container.querySelector(
8753 this.selectors.productMediaWrapper + "[data-media-id='" + mediaId + "']"
8754 );
8755
8756 var otherMedia = this.container.querySelectorAll(
8757 this.selectors.productMediaWrapper +
8758 ":not([data-media-id='" +
8759 mediaId +
8760 "'])"
8761 );
8762
8763 currentMedia.dispatchEvent(
8764 new CustomEvent('mediaHidden', {
8765 bubbles: true,
8766 cancelable: true
8767 })
8768 );
8769 newMedia.classList.remove(this.classes.hidden);
8770 newMedia.dispatchEvent(
8771 new CustomEvent('mediaVisible', {
8772 bubbles: true,
8773 cancelable: true
8774 })
8775 );
8776 otherMedia.forEach(
8777 function(el) {
8778 el.classList.add(this.classes.hidden);
8779 }.bind(this)
8780 );
8781 },
8782
8783 _handleMediaFocus: function(evt) {
8784 if (evt.keyCode !== slate.utils.keyboardKeys.ENTER) return;
8785
8786 var mediaId = evt.currentTarget.getAttribute('data-thumbnail-id');
8787
8788 var productMediaWrapper = this.container.querySelector(
8789 this.selectors.productMediaWrapper + "[data-media-id='" + mediaId + "']"
8790 );
8791 productMediaWrapper.focus();
8792 },
8793
8794 _initThumbnailSlider: function() {
8795 setTimeout(
8796 function() {
8797 this.slideshow = new theme.Slideshow(
8798 this.container.querySelector('[data-thumbnail-slider]'),
8799 {
8800 canUseTouchEvents: true,
8801 type: 'slide',
8802 slideActiveClass: 'slick-active',
8803 slidesToShow: 3,
8804 slidesToScroll: 3
8805 }
8806 );
8807
8808 this.settings.sliderActive = true;
8809 }.bind(this),
8810 250
8811 );
8812 },
8813
8814 _destroyThumbnailSlider: function() {
8815 var sliderButtons = this.container.querySelectorAll(
8816 '[data-slider-button]'
8817 );
8818 var sliderTrack = this.container.querySelector('[data-slider-track]');
8819 var sliderItems = sliderTrack.querySelectorAll('[data-slider-item');
8820 this.settings.sliderActive = false;
8821
8822 if (sliderTrack) {
8823 sliderTrack.removeAttribute('style');
8824 sliderItems.forEach(function(sliderItem) {
8825 var sliderItemLink = sliderItem.querySelector(
8826 '[data-slider-item-link]'
8827 );
8828 sliderItem.classList.remove('slick-active');
8829 sliderItem.removeAttribute('style');
8830 sliderItem.removeAttribute('tabindex');
8831 sliderItem.removeAttribute('aria-hidden');
8832 sliderItemLink.removeAttribute('tabindex');
8833 });
8834 }
8835
8836 sliderButtons.forEach(function(sliderButton) {
8837 sliderButton.removeAttribute('aria-disabled');
8838 });
8839
8840 this.slideshow.destroy();
8841 this.slideshow = null;
8842 },
8843
8844 _liveRegionText: function(variant) {
8845 // Dummy content for live region
8846 var liveRegionText =
8847 '[Availability] [Regular] [$$] [Sale] [$]. [UnitPrice] [$$$]';
8848
8849 if (!this.productState.available) {
8850 liveRegionText = theme.strings.unavailable;
8851 return liveRegionText;
8852 }
8853
8854 // Update availability
8855 var availability = this.productState.soldOut
8856 ? theme.strings.soldOut + ','
8857 : '';
8858 liveRegionText = liveRegionText.replace('[Availability]', availability);
8859
8860 // Update pricing information
8861 var regularLabel = '';
8862 var regularPrice = theme.Currency.formatMoney(
8863 variant.price,
8864 theme.moneyFormat
8865 );
8866 var saleLabel = '';
8867 var salePrice = '';
8868 var unitLabel = '';
8869 var unitPrice = '';
8870
8871 if (this.productState.onSale) {
8872 regularLabel = theme.strings.regularPrice;
8873 regularPrice =
8874 theme.Currency.formatMoney(
8875 variant.compare_at_price,
8876 theme.moneyFormat
8877 ) + ',';
8878 saleLabel = theme.strings.sale;
8879 salePrice = theme.Currency.formatMoney(
8880 variant.price,
8881 theme.moneyFormat
8882 );
8883 }
8884
8885 if (this.productState.showUnitPrice) {
8886 unitLabel = theme.strings.unitPrice;
8887 unitPrice =
8888 theme.Currency.formatMoney(variant.unit_price, theme.moneyFormat) +
8889 ' ' +
8890 theme.strings.unitPriceSeparator +
8891 ' ' +
8892 this._getBaseUnit(variant);
8893 }
8894
8895 liveRegionText = liveRegionText
8896 .replace('[Regular]', regularLabel)
8897 .replace('[$$]', regularPrice)
8898 .replace('[Sale]', saleLabel)
8899 .replace('[$]', salePrice)
8900 .replace('[UnitPrice]', unitLabel)
8901 .replace('[$$$]', unitPrice)
8902 .trim();
8903
8904 return liveRegionText;
8905 },
8906
8907 _updateLiveRegion: function(evt) {
8908 var variant = evt.detail.variant;
8909 var liveRegion = this.container.querySelector(
8910 this.selectors.productStatus
8911 );
8912 liveRegion.innerHTML = this._liveRegionText(variant);
8913 liveRegion.setAttribute('aria-hidden', false);
8914 // hide content from accessibility tree after announcement
8915 setTimeout(function() {
8916 liveRegion.setAttribute('aria-hidden', true);
8917 }, 1000);
8918 },
8919
8920 _enableAddToCart: function(message) {
8921 this.addToCart.removeAttribute('aria-disabled');
8922 this.addToCart.setAttribute('aria-label', message);
8923 this.addToCartText.innerHTML = message;
8924 this.productForm.classList.remove(this.classes.variantSoldOut);
8925 },
8926
8927 _disableAddToCart: function(message) {
8928 message = message || theme.strings.unavailable;
8929 this.addToCart.setAttribute('aria-disabled', true);
8930 this.addToCart.setAttribute('aria-label', message);
8931 this.addToCartText.innerHTML = message;
8932 this.productForm.classList.add(this.classes.variantSoldOut);
8933 },
8934
8935 _updateAddToCart: function() {
8936 if (!this.productState.available) {
8937 this._disableAddToCart(theme.strings.unavailable);
8938 return;
8939 }
8940 if (this.productState.soldOut) {
8941 this._disableAddToCart(theme.strings.soldOut);
8942 return;
8943 }
8944
8945 this._enableAddToCart(theme.strings.addToCart);
8946 },
8947
8948 /**
8949 * The returned productState object keeps track of a number of properties about the current variant and product
8950 * Multiple functions within product.js leverage the productState object to determine how to update the page's UI
8951 * @param {object} evt - object returned from variant change event
8952 * @return {object} productState - current product variant's state
8953 * productState.available - true if current product options result in valid variant
8954 * productState.soldOut - true if variant is sold out
8955 * productState.onSale - true if variant is on sale
8956 * productState.showUnitPrice - true if variant has unit price value
8957 */
8958 _setProductState: function(evt) {
8959 var variant = evt.detail.variant;
8960
8961 if (!variant) {
8962 this.productState.available = false;
8963 return;
8964 }
8965
8966 this.productState.available = true;
8967 this.productState.soldOut = !variant.available;
8968 this.productState.onSale = variant.compare_at_price > variant.price;
8969 this.productState.showUnitPrice = !!variant.unit_price;
8970 },
8971
8972 _updateAvailability: function(evt) {
8973 // remove error message if one is showing
8974 this._hideErrorMessage();
8975
8976 // set product state
8977 this._setProductState(evt);
8978
8979 // update store availabilities info
8980 this._updateStoreAvailabilityContent(evt);
8981 // update form submit
8982 this._updateAddToCart();
8983 // update live region
8984 this._updateLiveRegion(evt);
8985
8986 this._updatePrice(evt);
8987 },
8988
8989 _updateStoreAvailabilityContent: function(evt) {
8990 if (!this.storeAvailability) {
8991 return;
8992 }
8993
8994 if (this.productState.available && !this.productState.soldOut) {
8995 this.storeAvailability.updateContent(evt.detail.variant.id);
8996 } else {
8997 this.storeAvailability.clearContent();
8998 }
8999 },
9000
9001 _updateMedia: function(evt) {
9002 var variant = evt.detail.variant;
9003 var mediaId = variant.featured_media.id;
9004 var sectionMediaId = this.settings.sectionId + '-' + mediaId;
9005
9006 this._switchMedia(sectionMediaId);
9007 this._setActiveThumbnail(sectionMediaId);
9008 },
9009
9010 _updatePrice: function(evt) {
9011 var variant = evt.detail.variant;
9012
9013 var priceContainer = this.container.querySelector(
9014 this.selectors.priceContainer
9015 );
9016 var regularPrices = priceContainer.querySelectorAll(
9017 this.selectors.regularPrice
9018 );
9019 var salePrice = priceContainer.querySelector(this.selectors.salePrice);
9020 var unitPrice = priceContainer.querySelector(this.selectors.unitPrice);
9021 var unitPriceBaseUnit = priceContainer.querySelector(
9022 this.selectors.unitPriceBaseUnit
9023 );
9024
9025 var formatRegularPrice = function(regularPriceElement, price) {
9026 regularPriceElement.innerHTML = theme.Currency.formatMoney(
9027 price,
9028 theme.moneyFormat
9029 );
9030 };
9031
9032 // Reset product price state
9033
9034 priceContainer.classList.remove(
9035 this.classes.productUnavailable,
9036 this.classes.productOnSale,
9037 this.classes.productUnitAvailable,
9038 this.classes.productSoldOut
9039 );
9040 priceContainer.removeAttribute('aria-hidden');
9041
9042 if (this.productPolicies) {
9043 this.productPolicies.classList.remove(this.classes.visibilityHidden);
9044 }
9045
9046 // Unavailable
9047 if (!this.productState.available) {
9048 priceContainer.classList.add(this.classes.productUnavailable);
9049 priceContainer.setAttribute('aria-hidden', true);
9050
9051 if (this.productPolicies) {
9052 this.productPolicies.classList.add(this.classes.visibilityHidden);
9053 }
9054 return;
9055 }
9056
9057 // Sold out
9058 if (this.productState.soldOut) {
9059 priceContainer.classList.add(this.classes.productSoldOut);
9060 }
9061
9062 // On sale
9063 if (this.productState.onSale) {
9064 regularPrices.forEach(function(regularPrice) {
9065 formatRegularPrice(regularPrice, variant.compare_at_price);
9066 });
9067
9068 salePrice.innerHTML = theme.Currency.formatMoney(
9069 variant.price,
9070 theme.moneyFormat
9071 );
9072 priceContainer.classList.add(this.classes.productOnSale);
9073 } else {
9074 // Regular price
9075 regularPrices.forEach(function(regularPrice) {
9076 formatRegularPrice(regularPrice, variant.price);
9077 });
9078 }
9079
9080 // Unit price
9081 if (this.productState.showUnitPrice) {
9082 unitPrice.innerHTML = theme.Currency.formatMoney(
9083 variant.unit_price,
9084 theme.moneyFormat
9085 );
9086 unitPriceBaseUnit.innerHTML = this._getBaseUnit(variant);
9087 priceContainer.classList.add(this.classes.productUnitAvailable);
9088 }
9089 },
9090
9091 _getBaseUnit: function(variant) {
9092 return variant.unit_price_measurement.reference_value === 1
9093 ? variant.unit_price_measurement.reference_unit
9094 : variant.unit_price_measurement.reference_value +
9095 variant.unit_price_measurement.reference_unit;
9096 },
9097
9098 _updateSKU: function(evt) {
9099 var variant = evt.detail.variant;
9100
9101 // Update the sku
9102 var sku = document.querySelector(this.selectors.SKU);
9103 if (!sku) return;
9104 sku.innerHTML = variant.sku;
9105 },
9106
9107 _enableZoom: function(el, index) {
9108 this.zoomPictures[index] = new theme.Zoom(el);
9109 },
9110
9111 _destroyZoom: function(index) {
9112 if (this.zoomPictures.length === 0) return;
9113 this.zoomPictures[index].unload();
9114 },
9115
9116 onUnload: function() {
9117 this.container.removeEventListener(
9118 'variantChange',
9119 this.eventHandlers.updateAvailability
9120 );
9121 this.container.removeEventListener(
9122 'variantImageChange',
9123 this.eventHandlers.updateMedia
9124 );
9125 this.container.removeEventListener(
9126 'variantPriceChange',
9127 this.eventHandlers.updatePrice
9128 );
9129 this.container.removeEventListener(
9130 'variantSKUChange',
9131 this.eventHandlers.updateSKU
9132 );
9133 theme.ProductVideo.removeSectionVideos(this.settings.sectionId);
9134 theme.ProductModel.removeSectionModels(this.settings.sectionId);
9135
9136 if (this.mqlSmall) {
9137 this.mqlSmall.removeListener(this.initMobileBreakpoint);
9138 }
9139
9140 if (this.mqlMediumUp) {
9141 this.mqlMediumUp.removeListener(this.initDesktopBreakpoint);
9142 }
9143 }
9144 });
9145
9146 return Product;
9147})();
9148
9149theme.ProductRecommendations = (function() {
9150 function ProductRecommendations(container) {
9151 var baseUrl = container.dataset.baseUrl;
9152 var productId = container.dataset.productId;
9153 var recommendationsSectionUrl =
9154 baseUrl +
9155 '?section_id=product-recommendations&product_id=' +
9156 productId +
9157 '&limit=4';
9158
9159 window.performance.mark(
9160 'debut:product:fetch_product_recommendations.start'
9161 );
9162
9163 fetch(recommendationsSectionUrl)
9164 .then(function(response) {
9165 return response.text();
9166 })
9167 .then(function(productHtml) {
9168 if (productHtml.trim() === '') return;
9169
9170 container.innerHTML = productHtml;
9171 container.innerHTML = container.firstElementChild.innerHTML;
9172
9173 window.performance.mark(
9174 'debut:product:fetch_product_recommendations.end'
9175 );
9176
9177 performance.measure(
9178 'debut:product:fetch_product_recommendations',
9179 'debut:product:fetch_product_recommendations.start',
9180 'debut:product:fetch_product_recommendations.end'
9181 );
9182 });
9183 }
9184
9185 return ProductRecommendations;
9186})();
9187
9188theme.Quotes = (function() {
9189 var config = {
9190 mediaQuerySmall: 'screen and (max-width: 749px)',
9191 mediaQueryMediumUp: 'screen and (min-width: 750px)',
9192 slideCount: 0
9193 };
9194
9195 var defaults = {
9196 canUseKeyboardArrows: false,
9197 type: 'slide',
9198 slidesToShow: 3
9199 };
9200
9201 function Quotes(container) {
9202 this.container = container;
9203 var sectionId = container.getAttribute('data-section-id');
9204 this.slider = document.getElementById('Quotes-' + sectionId);
9205
9206 this.sliderActive = false;
9207
9208 this.mobileOptions = Object.assign({}, defaults, {
9209 canUseTouchEvents: true,
9210 slidesToShow: 1
9211 });
9212
9213 this.desktopOptions = Object.assign({}, defaults, {
9214 slidesToShow: Math.min(
9215 defaults.slidesToShow,
9216 this.slider.getAttribute('data-count')
9217 )
9218 });
9219
9220 this.initMobileSlider = this._initMobileSlider.bind(this);
9221 this.initDesktopSlider = this._initDesktopSlider.bind(this);
9222
9223 this.mqlSmall = window.matchMedia(config.mediaQuerySmall);
9224 this.mqlSmall.addListener(this.initMobileSlider);
9225
9226 this.mqlMediumUp = window.matchMedia(config.mediaQueryMediumUp);
9227 this.mqlMediumUp.addListener(this.initDesktopSlider);
9228
9229 this.initMobileSlider();
9230 this.initDesktopSlider();
9231 }
9232
9233 Quotes.prototype = Object.assign({}, Quotes.prototype, {
9234 onUnload: function() {
9235 this.mqlSmall.removeListener(this.initMobileSlider);
9236 this.mqlMediumUp.removeListener(this.initDesktopSlider);
9237 this.slideshow.destroy();
9238 },
9239
9240 // eslint-disable-next-line no-unused-vars
9241 onBlockSelect: function(evt) {
9242 var slide = document.querySelector(
9243 '.quotes-slide--' + evt.detail.blockId
9244 );
9245 var slideIndex = Number(slide.getAttribute('data-slider-slide-index'));
9246
9247 if (this.mqlMediumUp.matches) {
9248 slideIndex = Math.max(
9249 0,
9250 Math.min(slideIndex, this.slideshow.slides.length - 3)
9251 );
9252 }
9253
9254 this.slideshow.goToSlideByIndex(slideIndex);
9255 },
9256
9257 _initMobileSlider: function() {
9258 if (this.mqlSmall.matches) {
9259 this._initSlider(this.mobileOptions);
9260 }
9261 },
9262
9263 _initDesktopSlider: function() {
9264 if (this.mqlMediumUp.matches) {
9265 this._initSlider(this.desktopOptions);
9266 }
9267 },
9268
9269 // eslint-disable-next-line no-unused-vars
9270 _initSlider: function(args) {
9271 if (this.sliderActive) {
9272 this.slideshow.destroy();
9273 this.sliderActive = false;
9274 }
9275
9276 this.slideshow = new theme.Slideshow(this.container, args);
9277 this.sliderActive = true;
9278 }
9279 });
9280
9281 return Quotes;
9282})();
9283
9284theme.SlideshowSection = (function() {
9285 var selectors = {
9286 sliderMobileContentIndex: '[data-slider-mobile-content-index]'
9287 };
9288
9289 function SlideshowSection(container) {
9290 var sectionId = container.dataset.sectionId;
9291
9292 this.container = container;
9293 this.eventHandlers = {};
9294 this.slideshowDom = container.querySelector('#Slideshow-' + sectionId);
9295 this.sliderMobileContentIndex = container.querySelectorAll(
9296 selectors.sliderMobileContentIndex
9297 );
9298
9299 this.slideshow = new theme.Slideshow(container, {
9300 autoplay: this.slideshowDom.getAttribute('data-autorotate') === 'true',
9301 slideInterval: this.slideshowDom.getAttribute('data-speed')
9302 });
9303 this._setupEventListeners();
9304 }
9305
9306 return SlideshowSection;
9307})();
9308
9309theme.SlideshowSection.prototype = Object.assign(
9310 {},
9311 theme.SlideshowSection.prototype,
9312 {
9313 _setupEventListeners: function() {
9314 this.eventHandlers.onSliderSlideChanged = function(event) {
9315 this._onSliderSlideChanged(event.detail);
9316 }.bind(this);
9317
9318 this.container.addEventListener(
9319 'slider_slide_changed',
9320 this.eventHandlers.onSliderSlideChanged
9321 );
9322 },
9323
9324 _onSliderSlideChanged: function(slideIndex) {
9325 var activeClass = 'slideshow__text-content--mobile-active';
9326
9327 this.sliderMobileContentIndex.forEach(function(element) {
9328 if (
9329 Number(element.getAttribute('data-slider-mobile-content-index')) ===
9330 slideIndex
9331 ) {
9332 element.classList.add(activeClass);
9333 } else {
9334 element.classList.remove(activeClass);
9335 }
9336 });
9337 },
9338
9339 onUnload: function() {
9340 this.slideshow.destroy();
9341 },
9342
9343 onBlockSelect: function(evt) {
9344 if (this.slideshow.adaptHeight) {
9345 this.slideshow.setSlideshowHeight();
9346 }
9347
9348 // Get slide's index using theme editor's id
9349 var slide = this.container.querySelector(
9350 '.slideshow__slide--' + evt.detail.blockId
9351 );
9352 var slideIndex = slide.getAttribute('data-slider-slide-index');
9353
9354 // Go to selected slide, pause auto-rotate
9355 this.slideshow.setSlide(slideIndex);
9356 this.slideshow.stopAutoplay();
9357 },
9358
9359 onBlockDeselect: function() {
9360 // Resume auto-rotate
9361 this.slideshow.startAutoplay();
9362 }
9363 }
9364);
9365
9366window.theme = window.theme || {};
9367
9368theme.StoreAvailability = (function() {
9369 var selectors = {
9370 storeAvailabilityModalOpen: '[data-store-availability-modal-open]',
9371 storeAvailabilityModalProductTitle:
9372 '[data-store-availability-modal-product-title]',
9373 storeAvailabilityModalVariantTitle:
9374 '[data-store-availability-modal-variant-title]'
9375 };
9376
9377 var classes = {
9378 hidden: 'hide'
9379 };
9380
9381 function StoreAvailability(container) {
9382 this.container = container;
9383 this.productTitle = this.container.dataset.productTitle;
9384 this.hasOnlyDefaultVariant =
9385 this.container.dataset.hasOnlyDefaultVariant === 'true';
9386 }
9387
9388 StoreAvailability.prototype = Object.assign({}, StoreAvailability.prototype, {
9389 updateContent: function(variantId) {
9390 var variantSectionUrl =
9391 this.container.dataset.baseUrl +
9392 '/variants/' +
9393 variantId +
9394 '/?section_id=store-availability';
9395 var self = this;
9396
9397 var storeAvailabilityModalOpen = self.container.querySelector(
9398 selectors.storeAvailabilityModalOpen
9399 );
9400
9401 this.container.style.opacity = 0.5;
9402 if (storeAvailabilityModalOpen) {
9403 storeAvailabilityModalOpen.disabled = true;
9404 storeAvailabilityModalOpen.setAttribute('aria-busy', true);
9405 }
9406
9407 fetch(variantSectionUrl)
9408 .then(function(response) {
9409 return response.text();
9410 })
9411 .then(function(storeAvailabilityHTML) {
9412 if (storeAvailabilityHTML.trim() === '') {
9413 return;
9414 }
9415 self.container.innerHTML = storeAvailabilityHTML;
9416 self.container.innerHTML = self.container.firstElementChild.innerHTML;
9417 self.container.style.opacity = 1;
9418
9419 // Need to query this again because we updated the DOM
9420 storeAvailabilityModalOpen = self.container.querySelector(
9421 selectors.storeAvailabilityModalOpen
9422 );
9423
9424 if (!storeAvailabilityModalOpen) {
9425 return;
9426 }
9427
9428 storeAvailabilityModalOpen.addEventListener(
9429 'click',
9430 self._onClickModalOpen.bind(self)
9431 );
9432
9433 self.modal = self._initModal();
9434 self._updateProductTitle();
9435 if (self.hasOnlyDefaultVariant) {
9436 self._hideVariantTitle();
9437 }
9438 });
9439 },
9440
9441 clearContent: function() {
9442 this.container.innerHTML = '';
9443 },
9444
9445 _onClickModalOpen: function() {
9446 this.container.dispatchEvent(
9447 new CustomEvent('storeAvailabilityModalOpened', {
9448 bubbles: true,
9449 cancelable: true
9450 })
9451 );
9452 },
9453
9454 _initModal: function() {
9455 return new window.Modals(
9456 'StoreAvailabilityModal',
9457 'store-availability-modal',
9458 {
9459 close: '.js-modal-close-store-availability-modal',
9460 closeModalOnClick: true,
9461 openClass: 'store-availabilities-modal--active'
9462 }
9463 );
9464 },
9465
9466 _updateProductTitle: function() {
9467 var storeAvailabilityModalProductTitle = this.container.querySelector(
9468 selectors.storeAvailabilityModalProductTitle
9469 );
9470 storeAvailabilityModalProductTitle.textContent = this.productTitle;
9471 },
9472
9473 _hideVariantTitle: function() {
9474 var storeAvailabilityModalVariantTitle = this.container.querySelector(
9475 selectors.storeAvailabilityModalVariantTitle
9476 );
9477 storeAvailabilityModalVariantTitle.classList.add(classes.hidden);
9478 }
9479 });
9480
9481 return StoreAvailability;
9482})();
9483
9484theme.VideoSection = (function() {
9485 function VideoSection(container) {
9486 container.querySelectorAll('.video').forEach(function(el) {
9487 theme.Video.init(el);
9488 theme.Video.editorLoadVideo(el.id);
9489 });
9490 }
9491
9492 return VideoSection;
9493})();
9494
9495theme.VideoSection.prototype = Object.assign({}, theme.VideoSection.prototype, {
9496 onUnload: function() {
9497 theme.Video.removeEvents();
9498 }
9499});
9500
9501theme.heros = {};
9502
9503theme.HeroSection = (function() {
9504 function HeroSection(container) {
9505 var sectionId = container.getAttribute('data-section-id');
9506 var hero = '#Hero-' + sectionId;
9507 theme.heros[hero] = new theme.Hero(hero, sectionId);
9508 }
9509
9510 return HeroSection;
9511})();
9512
9513window.theme = window.theme || {};
9514
9515var selectors = {
9516 disclosureLocale: '[data-disclosure-locale]',
9517 disclosureCurrency: '[data-disclosure-currency]'
9518};
9519
9520theme.FooterSection = (function() {
9521 function Footer(container) {
9522 this.container = container;
9523 this.cache = {};
9524 this.cacheSelectors();
9525
9526 if (this.cache.localeDisclosure) {
9527 this.localeDisclosure = new theme.Disclosure(this.cache.localeDisclosure);
9528 }
9529
9530 if (this.cache.currencyDisclosure) {
9531 this.currencyDisclosure = new theme.Disclosure(
9532 this.cache.currencyDisclosure
9533 );
9534 }
9535 }
9536
9537 Footer.prototype = Object.assign({}, Footer.prototype, {
9538 cacheSelectors: function() {
9539 this.cache = {
9540 localeDisclosure: this.container.querySelector(
9541 selectors.disclosureLocale
9542 ),
9543 currencyDisclosure: this.container.querySelector(
9544 selectors.disclosureCurrency
9545 )
9546 };
9547 },
9548
9549 onUnload: function() {
9550 if (this.cache.localeDisclosure) {
9551 this.localeDisclosure.destroy();
9552 }
9553
9554 if (this.cache.currencyDisclosure) {
9555 this.currencyDisclosure.destroy();
9556 }
9557 }
9558 });
9559
9560 return Footer;
9561})();
9562
9563
9564document.addEventListener('DOMContentLoaded', function() {
9565 var sections = new theme.Sections();
9566
9567 sections.register('cart-template', theme.Cart);
9568 sections.register('product', theme.Product);
9569 sections.register('collection-template', theme.Filters);
9570 sections.register('product-template', theme.Product);
9571 sections.register('header-section', theme.HeaderSection);
9572 sections.register('map', theme.Maps);
9573 sections.register('slideshow-section', theme.SlideshowSection);
9574 sections.register('store-availability', theme.StoreAvailability);
9575 sections.register('video-section', theme.VideoSection);
9576 sections.register('quotes', theme.Quotes);
9577 sections.register('hero-section', theme.HeroSection);
9578 sections.register('product-recommendations', theme.ProductRecommendations);
9579 sections.register('footer-section', theme.FooterSection);
9580
9581 theme.customerTemplates.init();
9582
9583 // Theme-specific selectors to make tables scrollable
9584 var tableSelectors = '.rte table,' + '.custom__item-inner--html table';
9585
9586 slate.rte.wrapTable({
9587 tables: document.querySelectorAll(tableSelectors),
9588 tableWrapperClass: 'scrollable-wrapper'
9589 });
9590
9591 // Theme-specific selectors to make iframes responsive
9592 var iframeSelectors =
9593 '.rte iframe[src*="youtube.com/embed"],' +
9594 '.rte iframe[src*="player.vimeo"],' +
9595 '.custom__item-inner--html iframe[src*="youtube.com/embed"],' +
9596 '.custom__item-inner--html iframe[src*="player.vimeo"]';
9597
9598 slate.rte.wrapIframe({
9599 iframes: document.querySelectorAll(iframeSelectors),
9600 iframeWrapperClass: 'video-wrapper'
9601 });
9602
9603 // Common a11y fixes
9604 slate.a11y.pageLinkFocus(
9605 document.getElementById(window.location.hash.substr(1))
9606 );
9607
9608 var inPageLink = document.querySelector('.in-page-link');
9609 if (inPageLink) {
9610 inPageLink.addEventListener('click', function(evt) {
9611 slate.a11y.pageLinkFocus(
9612 document.getElementById(evt.currentTarget.hash.substr(1))
9613 );
9614 });
9615 }
9616
9617 document.querySelectorAll('a[href="#"]').forEach(function(anchor) {
9618 anchor.addEventListener('click', function(evt) {
9619 evt.preventDefault();
9620 });
9621 });
9622
9623 slate.a11y.accessibleLinks({
9624 messages: {
9625 newWindow: theme.strings.newWindow,
9626 external: theme.strings.external,
9627 newWindowExternal: theme.strings.newWindowExternal
9628 },
9629 links: document.querySelectorAll(
9630 'a[href]:not([aria-describedby]), .product-single__thumbnail'
9631 )
9632 });
9633
9634 theme.FormStatus.init();
9635
9636 var selectors = {
9637 image: '[data-image]',
9638 lazyloaded: '.lazyloaded'
9639 };
9640
9641 document.addEventListener('lazyloaded', function(evt) {
9642 var image = evt.target;
9643
9644 removeImageLoadingAnimation(image);
9645
9646 if (document.body.classList.contains('template-index')) {
9647 var mainContent = document.getElementById('MainContent');
9648
9649 if (mainContent && mainContent.children && mainContent.children.length) {
9650 var firstSection = document.getElementsByClassName('index-section')[0];
9651
9652 if (!firstSection.contains(image)) return;
9653
9654 window.performance.mark('debut:index:first_image_visible');
9655 }
9656 }
9657
9658 if (image.hasAttribute('data-bgset')) {
9659 var innerImage = image.querySelector(selectors.lazyloaded);
9660
9661 if (innerImage) {
9662 var alt = image.getAttribute('data-alt');
9663 var src = innerImage.hasAttribute('data-src')
9664 ? innerImage.getAttribute('data-src')
9665 : image.getAttribute('data-bg');
9666
9667 image.setAttribute('alt', alt ? alt : '');
9668 image.setAttribute('src', src ? src : '');
9669 }
9670 }
9671
9672 if (!image.hasAttribute('data-image')) {
9673 return;
9674 }
9675 });
9676
9677 // When the theme loads, lazysizes might load images before the "lazyloaded"
9678 // event listener has been attached. When this happens, the following function
9679 // hides the loading placeholders.
9680 function onLoadHideLazysizesAnimation() {
9681 var alreadyLazyloaded = document.querySelectorAll('.lazyloaded');
9682 alreadyLazyloaded.forEach(function(image) {
9683 removeImageLoadingAnimation(image);
9684 });
9685 }
9686
9687 onLoadHideLazysizesAnimation();
9688
9689 document.addEventListener(
9690 'touchstart',
9691 function() {
9692 theme.Helpers.setTouch();
9693 },
9694 { once: true }
9695 );
9696
9697 if (document.fonts) {
9698 document.fonts.ready.then(function() {
9699 window.performance.mark('debut:fonts_loaded');
9700 });
9701 }
9702});
9703
9704// Youtube API callback
9705// eslint-disable-next-line no-unused-vars
9706function onYouTubeIframeAPIReady() {
9707 theme.Video.loadVideos();
9708 theme.ProductVideo.loadVideos(theme.ProductVideo.hosts.youtube);
9709}
9710
9711function removeImageLoadingAnimation(image) {
9712 // Remove loading animation
9713 var imageWrapper = image.hasAttribute('data-image-loading-animation')
9714 ? image
9715 : image.closest('[data-image-loading-animation]');
9716
9717 if (imageWrapper) {
9718 imageWrapper.removeAttribute('data-image-loading-animation');
9719 }
9720}
9721