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