· 6 years ago · Mar 07, 2020, 04:24 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 location.reload();
2996 itemIndex,
2997 $itemElement,
2998 $itemQtyInputs,
2999 value
3000 ) {
3001 var key = $itemElement.attr(attributes.cartItemKey);
3002 var index = $itemElement.attr(attributes.cartItemIndex);
3003
3004 var params = {
3005 url: '/cart/change.js',
3006 data: { quantity: value, line: index },
3007 dataType: 'json'
3008 };
3009
3010 $.post(params)
3011 .done(
3012 function(state) {
3013 if (state.item_count === 0) {
3014 this._emptyCart();
3015 } else {
3016 this._createCart(state);
3017
3018 if (value === 0) {
3019 this._showRemoveMessage($itemElement.clone());
3020 } else {
3021 var $lineItem = $('[data-cart-item-key="' + key + '"]');
3022 var item = this.getItem(key, state);
3023
3024 $(selectors.quantityInput, $lineItem).focus();
3025 this._updateLiveRegion(item);
3026 }
3027 }
3028
3029 this._setCartCountBubble(state.item_count);
3030 }.bind(this)
3031 )
3032 .fail(
3033 function() {
3034 this._showCartError($itemQtyInputs);
3035 }.bind(this)
3036 );
3037 },
3038
3039 getItem: function(key, state) {
3040 return state.items.find(function(item) {
3041 return item.key === key;
3042 });
3043 },
3044
3045 _liveRegionText: function(item) {
3046 // Dummy content for live region
3047 var liveRegionText =
3048 theme.strings.update +
3049 ': [QuantityLabel]: [Quantity], [Regular] [$$] [DiscountedPrice] [$]. [PriceInformation]';
3050
3051 // Update Quantity
3052 liveRegionText = liveRegionText
3053 .replace('[QuantityLabel]', theme.strings.quantity)
3054 .replace('[Quantity]', item.quantity);
3055
3056 // Update pricing information
3057 var regularLabel = '';
3058 var regularPrice = theme.Currency.formatMoney(
3059 item.original_line_price,
3060 theme.moneyFormat
3061 );
3062 var discountLabel = '';
3063 var discountPrice = '';
3064 var discountInformation = '';
3065
3066 if (item.original_line_price > item.final_line_price) {
3067 regularLabel = theme.strings.regularTotal;
3068
3069 discountLabel = theme.strings.discountedTotal;
3070 discountPrice = theme.Currency.formatMoney(
3071 item.final_line_price,
3072 theme.moneyFormat
3073 );
3074
3075 discountInformation = theme.strings.priceColumn;
3076 }
3077
3078 liveRegionText = liveRegionText
3079 .replace('[Regular]', regularLabel)
3080 .replace('[$$]', regularPrice)
3081 .replace('[DiscountedPrice]', discountLabel)
3082 .replace('[$]', discountPrice)
3083 .replace('[PriceInformation]', discountInformation)
3084 .trim();
3085
3086 return liveRegionText;
3087 },
3088
3089 _updateLiveRegion: function(item) {
3090 var $liveRegion = $(selectors.cartStatus);
3091 $liveRegion.html(this._liveRegionText(item)).attr('aria-hidden', false);
3092
3093 // hide content from accessibility tree after announcement
3094 setTimeout(function() {
3095 $liveRegion.attr('aria-hidden', true);
3096 }, 1000);
3097 },
3098
3099 _createCart: function(state) {
3100 var cartDiscountList = this._createCartDiscountList(state);
3101
3102 $(selectors.cartLineItems, this.$container).html(
3103 this._createLineItemList(state)
3104 );
3105
3106 this.setQuantityFormControllers();
3107
3108 $(selectors.cartNote, this.$container).val(state.note);
3109
3110 if (cartDiscountList.length === 0) {
3111 $(selectors.cartDiscountWrapper, this.$container)
3112 .html('')
3113 .addClass(classes.hide);
3114 } else {
3115 $(selectors.cartDiscountWrapper, this.$container)
3116 .html(cartDiscountList)
3117 .removeClass(classes.hide);
3118 }
3119
3120 $(selectors.cartSubtotal, this.$container).html(
3121 theme.Currency.formatMoney(
3122 state.total_price,
3123 theme.moneyFormatWithCurrency
3124 )
3125 );
3126 },
3127
3128 _createCartDiscountList: function(cart) {
3129 return $.map(
3130 cart.cart_level_discount_applications,
3131 function(discount) {
3132 var $discount = this.$cartDiscountTemplate.clone();
3133 $discount.find(selectors.cartDiscountTitle).text(discount.title);
3134 $discount
3135 .find(selectors.cartDiscountAmount)
3136 .html(
3137 theme.Currency.formatMoney(
3138 discount.total_allocated_amount,
3139 theme.moneyFormat
3140 )
3141 );
3142 return $discount[0];
3143 }.bind(this)
3144 );
3145 },
3146
3147 _createLineItemList: function(state) {
3148 return $.map(
3149 state.items,
3150 function(item, index) {
3151 var $item = this.$itemTemplate.clone();
3152 var $itemPriceList = this.$itemPriceListTemplate.clone();
3153
3154 this._setLineItemAttributes($item, item, index);
3155 this._setLineItemImage($item, item.featured_image);
3156
3157 $(selectors.cartItemTitle, $item)
3158 .text(item.product_title)
3159 .attr('href', item.url);
3160
3161 var productDetailsList = this._createProductDetailsList(
3162 item.product_has_only_default_variant,
3163 item.options_with_values,
3164 item.properties
3165 );
3166 this._setProductDetailsList($item, productDetailsList);
3167
3168 this._setItemRemove($item, item.title);
3169
3170 $itemPriceList.html(
3171 this._createItemPrice(
3172 item.original_price,
3173 item.final_price,
3174 this.$itemPriceListTemplate
3175 )
3176 );
3177
3178 if (item.unit_price_measurement) {
3179 $itemPriceList.append(
3180 this._createUnitPrice(
3181 item.unit_price,
3182 item.unit_price_measurement,
3183 this.$itemPriceListTemplate
3184 )
3185 );
3186 }
3187
3188 this._setItemPrice($item, $itemPriceList);
3189
3190 var itemDiscountList = this._createItemDiscountList(item);
3191 this._setItemDiscountList($item, itemDiscountList);
3192
3193 this._setQuantityInputs($item, item, index);
3194
3195 var itemLinePrice = this._createItemPrice(
3196 item.original_line_price,
3197 item.final_line_price,
3198 this.$itemLinePriceTemplate
3199 );
3200 this._setItemLinePrice($item, itemLinePrice);
3201
3202 return $item[0];
3203 }.bind(this)
3204 );
3205 },
3206
3207 _setLineItemAttributes: function($item, item, index) {
3208 $item
3209 .attr(attributes.cartItemKey, item.key)
3210 .attr(attributes.cartItemUrl, item.url)
3211 .attr(attributes.cartItemTitle, item.title)
3212 .attr(attributes.cartItemIndex, index + 1)
3213 .attr(attributes.cartItemQuantity, item.quantity);
3214 },
3215
3216 _setLineItemImage: function($item, featuredImage) {
3217 var $image = $(selectors.cartItemImage, $item);
3218
3219 var sizedImageUrl =
3220 featuredImage.url !== null
3221 ? theme.Images.getSizedImageUrl(featuredImage.url, 'x190')
3222 : null;
3223
3224 if (sizedImageUrl) {
3225 $image
3226 .attr('alt', featuredImage.alt)
3227 .attr('src', sizedImageUrl)
3228 .removeClass(classes.hide);
3229 } else {
3230 $image.remove();
3231 }
3232 },
3233
3234 _setProductDetailsList: function($item, productDetailsList) {
3235 var $itemDetails = $(selectors.cartItemDetails, $item);
3236
3237 if (productDetailsList.length === 0) {
3238 $itemDetails.addClass(classes.hide).text('');
3239 } else {
3240 $itemDetails.removeClass(classes.hide).html(productDetailsList);
3241 }
3242 },
3243
3244 _setItemPrice: function($item, price) {
3245 $(selectors.cartItemPrice, $item).html(price);
3246 },
3247
3248 _setItemDiscountList: function($item, discountList) {
3249 var $itemDiscountList = $(selectors.cartItemDiscountList, $item);
3250
3251 if (discountList.length === 0) {
3252 $itemDiscountList.html('').addClass(classes.hide);
3253 } else {
3254 $itemDiscountList.html(discountList).removeClass(classes.hide);
3255 }
3256 },
3257
3258 _setItemRemove: function($item, title) {
3259 $(selectors.cartRemove, $item).attr(
3260 'aria-label',
3261 theme.strings.removeLabel.replace('[product]', title)
3262 );
3263 },
3264
3265 _setQuantityInputs: function($item, item, index) {
3266 $(selectors.quantityInputMobile, $item)
3267 .attr('id', 'updates_' + item.key)
3268 .attr(attributes.quantityItem, index + 1)
3269 .val(item.quantity);
3270
3271 $(selectors.quantityInputDesktop, $item)
3272 .attr('id', 'updates_large_' + item.key)
3273 .attr(attributes.quantityItem, index + 1)
3274 .val(item.quantity);
3275
3276 $(selectors.quantityLabelMobile, $item).attr(
3277 'for',
3278 'updates_' + item.key
3279 );
3280
3281 $(selectors.quantityLabelDesktop, $item).attr(
3282 'for',
3283 'updates_large_' + item.key
3284 );
3285 },
3286
3287 setQuantityFormControllers: function() {
3288 if (this.mql.matches) {
3289 $(selectors.quantityInputDesktop).attr('name', 'updates[]');
3290 $(selectors.quantityInputMobile).removeAttr('name');
3291 } else {
3292 $(selectors.quantityInputMobile).attr('name', 'updates[]');
3293 $(selectors.quantityInputDesktop).removeAttr('name');
3294 }
3295 },
3296
3297 _setItemLinePrice: function($item, price) {
3298 $(selectors.cartItemLinePrice, $item).html(price);
3299 },
3300
3301 _createProductDetailsList: function(
3302 product_has_only_default_variant,
3303 options,
3304 properties
3305 ) {
3306 var optionsPropertiesHTML = [];
3307
3308 if (!product_has_only_default_variant) {
3309 optionsPropertiesHTML = optionsPropertiesHTML.concat(
3310 this._getOptionList(options)
3311 );
3312 }
3313
3314 if (properties !== null && Object.keys(properties).length !== 0) {
3315 optionsPropertiesHTML = optionsPropertiesHTML.concat(
3316 this._getPropertyList(properties)
3317 );
3318 }
3319
3320 return optionsPropertiesHTML;
3321 },
3322
3323 _getOptionList: function(options) {
3324 return $.map(
3325 options,
3326 function(option) {
3327 var $optionElement = this.$itemOptionTemplate.clone();
3328
3329 $optionElement
3330 .text(option.name + ': ' + option.value)
3331 .removeClass(classes.hide);
3332
3333 return $optionElement[0];
3334 }.bind(this)
3335 );
3336 },
3337
3338 _getPropertyList: function(properties) {
3339 var propertiesArray =
3340 properties !== null ? Object.entries(properties) : [];
3341
3342 return $.map(
3343 propertiesArray,
3344 function(property) {
3345 var $propertyElement = this.$itemPropertyTemplate.clone();
3346
3347 // Line item properties prefixed with an underscore are not to be displayed
3348 if (property[0].charAt(0) === '_') return;
3349
3350 // if the property value has a length of 0 (empty), don't display it
3351 if (property[1].length === 0) return;
3352
3353 $propertyElement
3354 .find(selectors.cartItemPropertyName)
3355 .text(property[0]);
3356
3357 if (property[0].indexOf('/uploads/') === -1) {
3358 $propertyElement
3359 .find(selectors.cartItemPropertyValue)
3360 .text(': ' + property[1]);
3361 } else {
3362 $propertyElement
3363 .find(selectors.cartItemPropertyValue)
3364 .html(
3365 ': <a href="' +
3366 property[1] +
3367 '"> ' +
3368 property[1].split('/').pop() +
3369 '</a>'
3370 );
3371 }
3372
3373 $propertyElement.removeClass(classes.hide);
3374
3375 return $propertyElement[0];
3376 }.bind(this)
3377 );
3378 },
3379
3380 _createItemPrice: function(original_price, final_price, $priceTemplate) {
3381 if (original_price !== final_price) {
3382 var $discountedPrice = $(
3383 selectors.cartItemDiscountedPriceGroup,
3384 $priceTemplate
3385 ).clone();
3386
3387 $(selectors.cartItemOriginalPrice, $discountedPrice).html(
3388 theme.Currency.formatMoney(original_price, theme.moneyFormat)
3389 );
3390 $(selectors.cartItemFinalPrice, $discountedPrice).html(
3391 theme.Currency.formatMoney(final_price, theme.moneyFormat)
3392 );
3393 $discountedPrice.removeClass(classes.hide);
3394
3395 return $discountedPrice[0];
3396 } else {
3397 var $regularPrice = $(
3398 selectors.cartItemRegularPriceGroup,
3399 $priceTemplate
3400 ).clone();
3401
3402 $(selectors.cartItemRegularPrice, $regularPrice).html(
3403 theme.Currency.formatMoney(original_price, theme.moneyFormat)
3404 );
3405
3406 $regularPrice.removeClass(classes.hide);
3407
3408 return $regularPrice[0];
3409 }
3410 },
3411
3412 _createUnitPrice: function(
3413 unitPrice,
3414 unitPriceMeasurement,
3415 $itemPriceGroup
3416 ) {
3417 var $unitPriceGroup = $(
3418 selectors.unitPriceGroup,
3419 $itemPriceGroup
3420 ).clone();
3421
3422 var unitPriceBaseUnit =
3423 (unitPriceMeasurement.reference_value !== 1
3424 ? unitPriceMeasurement.reference_value
3425 : '') + unitPriceMeasurement.reference_unit;
3426
3427 $(selectors.unitPriceBaseUnit, $unitPriceGroup).text(unitPriceBaseUnit);
3428 $(selectors.unitPrice, $unitPriceGroup).html(
3429 theme.Currency.formatMoney(unitPrice, theme.moneyFormat)
3430 );
3431
3432 $unitPriceGroup.removeClass(classes.hide);
3433
3434 return $unitPriceGroup[0];
3435 },
3436
3437 _createItemDiscountList: function(item) {
3438 return $.map(
3439 item.line_level_discount_allocations,
3440 function(discount) {
3441 var $discount = this.$itemDiscountTemplate.clone();
3442 $discount
3443 .find(selectors.cartItemDiscountTitle)
3444 .text(discount.discount_application.title);
3445 $discount
3446 .find(selectors.cartItemDiscountAmount)
3447 .html(
3448 theme.Currency.formatMoney(discount.amount, theme.moneyFormat)
3449 );
3450 return $discount[0];
3451 }.bind(this)
3452 );
3453 },
3454
3455 _showQuantityErrorMessages: function(itemElement) {
3456 $(selectors.cartQuantityErrorMessage, itemElement).text(
3457 theme.strings.quantityMinimumMessage
3458 );
3459
3460 $(selectors.cartQuantityErrorMessageWrapper, itemElement).removeClass(
3461 classes.hide
3462 );
3463
3464 $(selectors.inputQty, itemElement)
3465 .addClass(classes.inputError)
3466 .focus();
3467 },
3468
3469 _hideQuantityErrorMessage: function() {
3470 var $errorMessages = $(
3471 selectors.cartQuantityErrorMessageWrapper
3472 ).addClass(classes.hide);
3473
3474 $(selectors.cartQuantityErrorMessage, $errorMessages).text('');
3475
3476 $(selectors.inputQty, this.$container).removeClass(classes.inputError);
3477 },
3478
3479 _handleThumbnailClick: function(evt) {
3480 var url = $(evt.target)
3481 .closest(selectors.cartItem)
3482 .data('cart-item-url');
3483
3484 window.location.href = url;
3485 },
3486
3487 _onNoteChange: function(evt) {
3488 var note = evt.currentTarget.value;
3489 this._hideCartError();
3490 this._hideQuantityErrorMessage();
3491
3492 var params = {
3493 url: '/cart/update.js',
3494 data: { note: note },
3495 dataType: 'json'
3496 };
3497
3498 $.post(params).fail(
3499 function() {
3500 this._showCartError(evt.currentTarget);
3501 }.bind(this)
3502 );
3503 },
3504
3505 _showCartError: function(elementToFocus) {
3506 $(selectors.cartErrorMessage).text(theme.strings.cartError);
3507
3508 $(selectors.cartErrorMessageWrapper).removeClass(classes.hide);
3509
3510 elementToFocus.focus();
3511 },
3512
3513 _hideCartError: function() {
3514 $(selectors.cartErrorMessageWrapper).addClass(classes.hide);
3515 $(selectors.cartErrorMessage).text('');
3516 },
3517
3518 _onRemoveItem: function(evt) {
3519 evt.preventDefault();
3520 var $remove = $(evt.target);
3521 var $lineItem = $remove.closest(selectors.cartItem);
3522 var index = $lineItem.attr(attributes.cartItemIndex);
3523 this._hideCartError();
3524
3525 var params = {
3526 url: '/cart/change.js',
3527 data: { quantity: 0, line: index },
3528 dataType: 'json'
3529 };
3530
3531 $.post(params)
3532 .done(
3533 function(state) {
3534 if (state.item_count === 0) {
3535 this._emptyCart();
3536 } else {
3537 this._createCart(state);
3538 this._showRemoveMessage($lineItem.clone());
3539 }
3540
3541 this._setCartCountBubble(state.item_count);
3542 }.bind(this)
3543 )
3544 .fail(
3545 function() {
3546 this._showCartError(null);
3547 }.bind(this)
3548 );
3549 },
3550
3551 _showRemoveMessage: function(lineItem) {
3552 var index = lineItem.data('cart-item-index');
3553 var removeMessage = this._getRemoveMessage(lineItem);
3554 var $lineItemAtIndex;
3555
3556 if (index - 1 === 0) {
3557 $lineItemAtIndex = $('[data-cart-item-index="1"]', this.$container);
3558 $(removeMessage).insertBefore($lineItemAtIndex);
3559 } else {
3560 $lineItemAtIndex = $(
3561 '[data-cart-item-index="' + (index - 1) + '"]',
3562 this.$container
3563 );
3564 removeMessage.insertAfter($lineItemAtIndex);
3565 }
3566 removeMessage.focus();
3567 },
3568
3569 _getRemoveMessage: function(lineItem) {
3570 var formattedMessage = this._formatRemoveMessage(lineItem);
3571
3572 var $tableCell = $(selectors.cartTableCell, lineItem).clone();
3573 $tableCell
3574 .removeClass()
3575 .addClass(classes.cartRemovedProduct)
3576 .attr('colspan', '4')
3577 .html(formattedMessage);
3578
3579 lineItem
3580 .attr('role', 'alert')
3581 .html($tableCell)
3582 .attr('tabindex', '-1');
3583
3584 return lineItem;
3585 },
3586
3587 _formatRemoveMessage: function(lineItem) {
3588 var quantity = lineItem.data('cart-item-quantity');
3589 var url = lineItem.attr(attributes.cartItemUrl);
3590 var title = lineItem.attr(attributes.cartItemTitle);
3591
3592 return theme.strings.removedItemMessage
3593 .replace('[quantity]', quantity)
3594 .replace(
3595 '[link]',
3596 '<a ' +
3597 'href="' +
3598 url +
3599 '" class="text-link text-link--accent">' +
3600 title +
3601 '</a>'
3602 );
3603 },
3604
3605 _setCartCountBubble: function(quantity) {
3606 this.$cartCountBubble =
3607 this.$cartCountBubble || $(selectors.cartCountBubble);
3608 this.$cartCount = this.$cartCount || $(selectors.cartCount);
3609
3610 if (quantity > 0) {
3611 this.$cartCountBubble.removeClass(classes.hide);
3612 this.$cartCount.html(quantity);
3613 } else {
3614 this.$cartCountBubble.addClass(classes.hide);
3615 this.$cartCount.html('');
3616 }
3617 },
3618
3619 _emptyCart: function() {
3620 this.$emptyPageContent =
3621 this.$emptyPageContent ||
3622 $(selectors.emptyPageContent, this.$container);
3623 this.$cartWrapper =
3624 this.$cartWrapper || $(selectors.cartWrapper, this.$container);
3625
3626 this.$emptyPageContent.removeClass(classes.hide);
3627 this.$cartWrapper.addClass(classes.hide);
3628 },
3629
3630 cookiesEnabled: function() {
3631 var cookieEnabled = navigator.cookieEnabled;
3632
3633 if (!cookieEnabled) {
3634 document.cookie = 'testcookie';
3635 cookieEnabled = document.cookie.indexOf('testcookie') !== -1;
3636 }
3637 return cookieEnabled;
3638 }
3639 });
3640
3641 return Cart;
3642})();
3643
3644window.theme = window.theme || {};
3645
3646theme.Filters = (function() {
3647 var settings = {
3648 // Breakpoints from src/stylesheets/global/variables.scss.liquid
3649 mediaQueryMediumUp: 'screen and (min-width: 750px)'
3650 };
3651
3652 var selectors = {
3653 mainContent: '#MainContent',
3654 filterSelection: '#FilterTags',
3655 sortSelection: '#SortBy'
3656 };
3657
3658 var data = {
3659 sortBy: 'data-default-sortby'
3660 };
3661
3662 function Filters(container) {
3663 var $container = (this.$container = $(container));
3664
3665 this.$filterSelect = $(selectors.filterSelection, $container);
3666 this.$sortSelect = $(selectors.sortSelection, $container);
3667 this.$selects = $(selectors.filterSelection, $container).add(
3668 $(selectors.sortSelection, $container)
3669 );
3670
3671 this.defaultSort = this._getDefaultSortValue();
3672 this.$selects.removeClass('hidden');
3673
3674 this.$filterSelect.on('change', this._onFilterChange.bind(this));
3675 this.$sortSelect.on('change', this._onSortChange.bind(this));
3676 this._initBreakpoints();
3677 this._initParams();
3678 }
3679
3680 Filters.prototype = _.assignIn({}, Filters.prototype, {
3681 _initBreakpoints: function() {
3682 var self = this;
3683
3684 enquire.register(settings.mediaQueryMediumUp, {
3685 match: function() {
3686 self._resizeSelect(self.$selects);
3687 }
3688 });
3689 },
3690
3691 _initParams: function() {
3692 self.queryParams = {};
3693 if (location.search.length) {
3694 var aKeyValue;
3695 var aCouples = location.search.substr(1).split('&');
3696 for (var i = 0; i < aCouples.length; i++) {
3697 aKeyValue = aCouples[i].split('=');
3698 if (aKeyValue.length > 1) {
3699 self.queryParams[
3700 decodeURIComponent(aKeyValue[0])
3701 ] = decodeURIComponent(aKeyValue[1]);
3702 }
3703 }
3704 }
3705 },
3706
3707 _onSortChange: function() {
3708 self.queryParams.sort_by = this._getSortValue();
3709
3710 if (self.queryParams.page) {
3711 delete self.queryParams.page;
3712 }
3713 window.location.search = decodeURIComponent($.param(self.queryParams));
3714 },
3715
3716 _onFilterChange: function() {
3717 document.location.href = this._getFilterValue();
3718 },
3719
3720 _getFilterValue: function() {
3721 return this.$filterSelect.val();
3722 },
3723
3724 _getSortValue: function() {
3725 return this.$sortSelect.val() || this.defaultSort;
3726 },
3727
3728 _getDefaultSortValue: function() {
3729 return this.$sortSelect.attr(data.sortBy);
3730 },
3731
3732 _resizeSelect: function($selection) {
3733 $selection.each(function() {
3734 var $this = $(this);
3735 var arrowWidth = 10;
3736 // create test element
3737 var text = $this.find('option:selected').text();
3738 var $test = $('<span>').html(text);
3739
3740 // add to body, get width, and get out
3741 $test.appendTo('body');
3742 var width = $test.width();
3743 $test.remove();
3744
3745 // set select width
3746 $this.width(width + arrowWidth);
3747 });
3748 },
3749
3750 onUnload: function() {
3751 this.$filterSelect.off('change', this._onFilterChange);
3752 this.$sortSelect.off('change', this._onSortChange);
3753 }
3754 });
3755
3756 return Filters;
3757})();
3758
3759window.theme = window.theme || {};
3760
3761theme.HeaderSection = (function() {
3762 function Header() {
3763 theme.Header.init();
3764 theme.MobileNav.init();
3765 theme.Search.init();
3766 }
3767
3768 Header.prototype = _.assignIn({}, Header.prototype, {
3769 onUnload: function() {
3770 theme.Header.unload();
3771 }
3772 });
3773
3774 return Header;
3775})();
3776
3777theme.Maps = (function() {
3778 var config = {
3779 zoom: 14
3780 };
3781 var apiStatus = null;
3782 var mapsToLoad = [];
3783
3784 var errors = {
3785 addressNoResults: theme.strings.addressNoResults,
3786 addressQueryLimit: theme.strings.addressQueryLimit,
3787 addressError: theme.strings.addressError,
3788 authError: theme.strings.authError
3789 };
3790
3791 var selectors = {
3792 section: '[data-section-type="map"]',
3793 map: '[data-map]',
3794 mapOverlay: '[data-map-overlay]'
3795 };
3796
3797 var classes = {
3798 mapError: 'map-section--load-error',
3799 errorMsg: 'map-section__error errors text-center'
3800 };
3801
3802 // Global function called by Google on auth errors.
3803 // Show an auto error message on all map instances.
3804 // eslint-disable-next-line camelcase, no-unused-vars
3805 window.gm_authFailure = function() {
3806 if (!Shopify.designMode) {
3807 return;
3808 }
3809
3810 $(selectors.section).addClass(classes.mapError);
3811 $(selectors.map).remove();
3812 $(selectors.mapOverlay).after(
3813 '<div class="' +
3814 classes.errorMsg +
3815 '">' +
3816 theme.strings.authError +
3817 '</div>'
3818 );
3819 };
3820
3821 function Map(container) {
3822 this.$container = $(container);
3823 this.$map = this.$container.find(selectors.map);
3824 this.key = this.$map.data('api-key');
3825
3826 if (typeof this.key === 'undefined') {
3827 return;
3828 }
3829
3830 if (apiStatus === 'loaded') {
3831 this.createMap();
3832 } else {
3833 mapsToLoad.push(this);
3834
3835 if (apiStatus !== 'loading') {
3836 apiStatus = 'loading';
3837 if (typeof window.google === 'undefined') {
3838 $.getScript(
3839 'https://maps.googleapis.com/maps/api/js?key=' + this.key
3840 ).then(function() {
3841 apiStatus = 'loaded';
3842 initAllMaps();
3843 });
3844 }
3845 }
3846 }
3847 }
3848
3849 function initAllMaps() {
3850 // API has loaded, load all Map instances in queue
3851 $.each(mapsToLoad, function(index, instance) {
3852 instance.createMap();
3853 });
3854 }
3855
3856 function geolocate($map) {
3857 var deferred = $.Deferred();
3858 var geocoder = new google.maps.Geocoder();
3859 var address = $map.data('address-setting');
3860
3861 geocoder.geocode({ address: address }, function(results, status) {
3862 if (status !== google.maps.GeocoderStatus.OK) {
3863 deferred.reject(status);
3864 }
3865
3866 deferred.resolve(results);
3867 });
3868
3869 return deferred;
3870 }
3871
3872 Map.prototype = _.assignIn({}, Map.prototype, {
3873 createMap: function() {
3874 var $map = this.$map;
3875
3876 return geolocate($map)
3877 .then(
3878 function(results) {
3879 var mapOptions = {
3880 zoom: config.zoom,
3881 center: results[0].geometry.location,
3882 draggable: false,
3883 clickableIcons: false,
3884 scrollwheel: false,
3885 disableDoubleClickZoom: true,
3886 disableDefaultUI: true
3887 };
3888
3889 var map = (this.map = new google.maps.Map($map[0], mapOptions));
3890 var center = (this.center = map.getCenter());
3891
3892 //eslint-disable-next-line no-unused-vars
3893 var marker = new google.maps.Marker({
3894 map: map,
3895 position: map.getCenter()
3896 });
3897
3898 google.maps.event.addDomListener(
3899 window,
3900 'resize',
3901 $.debounce(250, function() {
3902 google.maps.event.trigger(map, 'resize');
3903 map.setCenter(center);
3904 $map.removeAttr('style');
3905 })
3906 );
3907 }.bind(this)
3908 )
3909 .fail(function() {
3910 var errorMessage;
3911
3912 switch (status) {
3913 case 'ZERO_RESULTS':
3914 errorMessage = errors.addressNoResults;
3915 break;
3916 case 'OVER_QUERY_LIMIT':
3917 errorMessage = errors.addressQueryLimit;
3918 break;
3919 case 'REQUEST_DENIED':
3920 errorMessage = errors.authError;
3921 break;
3922 default:
3923 errorMessage = errors.addressError;
3924 break;
3925 }
3926
3927 // Show errors only to merchant in the editor.
3928 if (Shopify.designMode) {
3929 $map
3930 .parent()
3931 .addClass(classes.mapError)
3932 .html(
3933 '<div class="' +
3934 classes.errorMsg +
3935 '">' +
3936 errorMessage +
3937 '</div>'
3938 );
3939 }
3940 });
3941 },
3942
3943 onUnload: function() {
3944 if (this.$map.length === 0) {
3945 return;
3946 }
3947 google.maps.event.clearListeners(this.map, 'resize');
3948 }
3949 });
3950
3951 return Map;
3952})();
3953
3954/* eslint-disable no-new */
3955theme.Product = (function() {
3956 function Product(container) {
3957 var $container = (this.$container = $(container));
3958 var sectionId = $container.attr('data-section-id');
3959 this.ajaxEnabled = $container.data('ajax-enabled');
3960
3961 this.settings = {
3962 // Breakpoints from src/stylesheets/global/variables.scss.liquid
3963 mediaQueryMediumUp: 'screen and (min-width: 750px)',
3964 mediaQuerySmall: 'screen and (max-width: 749px)',
3965 bpSmall: false,
3966 enableHistoryState: $container.data('enable-history-state') || false,
3967 namespace: '.slideshow-' + sectionId,
3968 sectionId: sectionId,
3969 sliderActive: false,
3970 zoomEnabled: false
3971 };
3972
3973 this.selectors = {
3974 addToCart: '[data-add-to-cart]',
3975 addToCartText: '[data-add-to-cart-text]',
3976 cartCount: '[data-cart-count]',
3977 cartCountBubble: '[data-cart-count-bubble]',
3978 cartPopup: '[data-cart-popup]',
3979 cartPopupCartQuantity: '[data-cart-popup-cart-quantity]',
3980 cartPopupClose: '[data-cart-popup-close]',
3981 cartPopupDismiss: '[data-cart-popup-dismiss]',
3982 cartPopupImage: '[data-cart-popup-image]',
3983 cartPopupImageWrapper: '[data-cart-popup-image-wrapper]',
3984 cartPopupImagePlaceholder: '[data-cart-popup-image-placeholder]',
3985 cartPopupPlaceholderSize: '[data-placeholder-size]',
3986 cartPopupProductDetails: '[data-cart-popup-product-details]',
3987 cartPopupQuantity: '[data-cart-popup-quantity]',
3988 cartPopupQuantityLabel: '[data-cart-popup-quantity-label]',
3989 cartPopupTitle: '[data-cart-popup-title]',
3990 cartPopupWrapper: '[data-cart-popup-wrapper]',
3991 loader: '[data-loader]',
3992 loaderStatus: '[data-loader-status]',
3993 quantity: '[data-quantity-input]',
3994 SKU: '.variant-sku',
3995 productStatus: '[data-product-status]',
3996 originalSelectorId: '#ProductSelect-' + sectionId,
3997 productForm: '[data-product-form]',
3998 errorMessage: '[data-error-message]',
3999 errorMessageWrapper: '[data-error-message-wrapper]',
4000 productImageWraps: '.product-single__photo',
4001 productThumbImages: '.product-single__thumbnail--' + sectionId,
4002 productThumbs: '.product-single__thumbnails-' + sectionId,
4003 productThumbListItem: '.product-single__thumbnails-item',
4004 productFeaturedImage: '.product-featured-img',
4005 productThumbsWrapper: '.thumbnails-wrapper',
4006 saleLabel: '.product-price__sale-label-' + sectionId,
4007 singleOptionSelector: '.single-option-selector-' + sectionId,
4008 shopifyPaymentButton: '.shopify-payment-button',
4009 priceContainer: '[data-price]',
4010 regularPrice: '[data-regular-price]',
4011 salePrice: '[data-sale-price]',
4012 unitPrice: '[data-unit-price]',
4013 unitPriceBaseUnit: '[data-unit-price-base-unit]',
4014 productPolicies: '[data-product-policies]'
4015 };
4016
4017 this.classes = {
4018 cartPopupWrapperHidden: 'cart-popup-wrapper--hidden',
4019 hidden: 'hide',
4020 visibilityHidden: 'visibility-hidden',
4021 inputError: 'input--error',
4022 productOnSale: 'price--on-sale',
4023 productUnitAvailable: 'price--unit-available',
4024 productUnavailable: 'price--unavailable',
4025 productSoldOut: 'price--sold-out',
4026 cartImage: 'cart-popup-item__image',
4027 productFormErrorMessageWrapperHidden:
4028 'product-form__error-message-wrapper--hidden',
4029 activeClass: 'active-thumb'
4030 };
4031
4032 this.$quantityInput = $(this.selectors.quantity, $container);
4033 this.$errorMessageWrapper = $(
4034 this.selectors.errorMessageWrapper,
4035 $container
4036 );
4037 this.$addToCart = $(this.selectors.addToCart, $container);
4038 this.$addToCartText = $(this.selectors.addToCartText, this.$addToCart);
4039 this.$shopifyPaymentButton = $(
4040 this.selectors.shopifyPaymentButton,
4041 $container
4042 );
4043 this.$productPolicies = $(this.selectors.productPolicies, $container);
4044
4045 this.$loader = $(this.selectors.loader, this.$addToCart);
4046 this.$loaderStatus = $(this.selectors.loaderStatus, $container);
4047
4048 // Stop parsing if we don't have the product json script tag when loading
4049 // section in the Theme Editor
4050 if (!$('#ProductJson-' + sectionId).html()) {
4051 return;
4052 }
4053
4054 this.productSingleObject = JSON.parse(
4055 document.getElementById('ProductJson-' + sectionId).innerHTML
4056 );
4057
4058 this.settings.zoomEnabled = $(this.selectors.productImageWraps).hasClass(
4059 'js-zoom-enabled'
4060 );
4061
4062 this._initBreakpoints();
4063 this._stringOverrides();
4064 this._initVariants();
4065 this._initImageSwitch();
4066 this._initAddToCart();
4067 this._setActiveThumbnail();
4068 }
4069
4070 Product.prototype = _.assignIn({}, Product.prototype, {
4071 _stringOverrides: function() {
4072 theme.productStrings = theme.productStrings || {};
4073 $.extend(theme.strings, theme.productStrings);
4074 },
4075
4076 _initBreakpoints: function() {
4077 var self = this;
4078
4079 enquire.register(this.settings.mediaQuerySmall, {
4080 match: function() {
4081 // initialize thumbnail slider on mobile if more than three thumbnails
4082 if ($(self.selectors.productThumbImages).length > 3) {
4083 self._initThumbnailSlider();
4084 }
4085
4086 // destroy image zooming if enabled
4087 if (self.settings.zoomEnabled) {
4088 $(self.selectors.productImageWraps).each(function() {
4089 _destroyZoom(this);
4090 });
4091 }
4092
4093 self.settings.bpSmall = true;
4094 },
4095 unmatch: function() {
4096 if (self.settings.sliderActive) {
4097 self._destroyThumbnailSlider();
4098 }
4099
4100 self.settings.bpSmall = false;
4101 }
4102 });
4103
4104 enquire.register(this.settings.mediaQueryMediumUp, {
4105 match: function() {
4106 if (self.settings.zoomEnabled) {
4107 $(self.selectors.productImageWraps).each(function() {
4108 _enableZoom(this);
4109 });
4110 }
4111 }
4112 });
4113 },
4114
4115 _initVariants: function() {
4116 var options = {
4117 $container: this.$container,
4118 enableHistoryState:
4119 this.$container.data('enable-history-state') || false,
4120 singleOptionSelector: this.selectors.singleOptionSelector,
4121 originalSelectorId: this.selectors.originalSelectorId,
4122 product: this.productSingleObject
4123 };
4124
4125 this.variants = new slate.Variants(options);
4126
4127 this.$container.on(
4128 'variantChange' + this.settings.namespace,
4129 this._updateAvailability.bind(this)
4130 );
4131 this.$container.on(
4132 'variantImageChange' + this.settings.namespace,
4133 this._updateImages.bind(this)
4134 );
4135 this.$container.on(
4136 'variantPriceChange' + this.settings.namespace,
4137 this._updatePrice.bind(this)
4138 );
4139 this.$container.on(
4140 'variantSKUChange' + this.settings.namespace,
4141 this._updateSKU.bind(this)
4142 );
4143 },
4144
4145 _initImageSwitch: function() {
4146 if (!$(this.selectors.productThumbImages).length) {
4147 return;
4148 }
4149
4150 var self = this;
4151
4152 $(this.selectors.productThumbImages)
4153 .on('click', function(evt) {
4154 evt.preventDefault();
4155 var $el = $(this);
4156
4157 var imageId = $el.data('thumbnail-id');
4158
4159 self._switchImage(imageId);
4160 self._setActiveThumbnail(imageId);
4161 })
4162 .on('keyup', self._handleImageFocus.bind(self));
4163 },
4164
4165 _initAddToCart: function() {
4166 $(this.selectors.productForm, this.$container).on(
4167 'submit',
4168 function(evt) {
4169 if (this.$addToCart.is('[aria-disabled]')) {
4170 evt.preventDefault();
4171 return;
4172 }
4173
4174 if (!this.ajaxEnabled) return;
4175
4176 evt.preventDefault();
4177
4178 this.$previouslyFocusedElement = $(':focus');
4179
4180 var isInvalidQuantity = this.$quantityInput.val() <= 0;
4181
4182 if (isInvalidQuantity) {
4183 this._showErrorMessage(theme.strings.quantityMinimumMessage);
4184 return;
4185 }
4186
4187 if (!isInvalidQuantity && this.ajaxEnabled) {
4188 // disable the addToCart and dynamic checkout button while
4189 // request/cart popup is loading and handle loading state
4190 this._handleButtonLoadingState(true);
4191 var $data = $(this.selectors.productForm, this.$container);
4192 this._addItemToCart($data);
4193 return;
4194 }
4195 }.bind(this)
4196 );
4197 },
4198
4199 _addItemToCart: function(data) {
4200 var params = {
4201 url: '/cart/add.js',
4202 data: $(data).serialize(),
4203 dataType: 'json'
4204 };
4205
4206 $.post(params)
4207 .done(
4208 function(item) {
4209 this._hideErrorMessage();
4210 this._setupCartPopup(item);
4211 }.bind(this)
4212 )
4213 .fail(
4214 function(response) {
4215 this.$previouslyFocusedElement.focus();
4216 var errorMessage = response.responseJSON
4217 ? response.responseJSON.description
4218 : theme.strings.cartError;
4219 this._showErrorMessage(errorMessage);
4220 this._handleButtonLoadingState(false);
4221 }.bind(this)
4222 );
4223 },
4224
4225 _handleButtonLoadingState: function(isLoading) {
4226 if (isLoading) {
4227 this.$addToCart.attr('aria-disabled', true);
4228 this.$addToCartText.addClass(this.classes.hidden);
4229 this.$loader.removeClass(this.classes.hidden);
4230 this.$shopifyPaymentButton.attr('disabled', true);
4231 this.$loaderStatus.attr('aria-hidden', false);
4232 } else {
4233 this.$addToCart.removeAttr('aria-disabled');
4234 this.$addToCartText.removeClass(this.classes.hidden);
4235 this.$loader.addClass(this.classes.hidden);
4236 this.$shopifyPaymentButton.removeAttr('disabled');
4237 this.$loaderStatus.attr('aria-hidden', true);
4238 }
4239 },
4240
4241 _showErrorMessage: function(errorMessage) {
4242 $(this.selectors.errorMessage, this.$container).html(errorMessage);
4243
4244 if (this.$quantityInput.length !== 0) {
4245 this.$quantityInput.addClass(this.classes.inputError);
4246 }
4247
4248 this.$errorMessageWrapper
4249 .removeClass(this.classes.productFormErrorMessageWrapperHidden)
4250 .attr('aria-hidden', true)
4251 .removeAttr('aria-hidden');
4252 },
4253
4254 _hideErrorMessage: function() {
4255 this.$errorMessageWrapper.addClass(
4256 this.classes.productFormErrorMessageWrapperHidden
4257 );
4258
4259 if (this.$quantityInput.length !== 0) {
4260 this.$quantityInput.removeClass(this.classes.inputError);
4261 }
4262 },
4263
4264 _setupCartPopup: function(item) {
4265 this.$cartPopup = this.$cartPopup || $(this.selectors.cartPopup);
4266 this.$cartPopupWrapper =
4267 this.$cartPopupWrapper || $(this.selectors.cartPopupWrapper);
4268 this.$cartPopupTitle =
4269 this.$cartPopupTitle || $(this.selectors.cartPopupTitle);
4270 this.$cartPopupQuantity =
4271 this.$cartPopupQuantity || $(this.selectors.cartPopupQuantity);
4272 this.$cartPopupQuantityLabel =
4273 this.$cartPopupQuantityLabel ||
4274 $(this.selectors.cartPopupQuantityLabel);
4275 this.$cartPopupClose =
4276 this.$cartPopupClose || $(this.selectors.cartPopupClose);
4277 this.$cartPopupDismiss =
4278 this.$cartPopupDismiss || $(this.selectors.cartPopupDismiss);
4279 this.$cartPopupImagePlaceholder =
4280 this.$cartPopupImagePlaceholder ||
4281 $(this.selectors.cartPopupImagePlaceholder);
4282
4283 this._setupCartPopupEventListeners();
4284
4285 this._updateCartPopupContent(item);
4286 },
4287
4288 _updateCartPopupContent: function(item) {
4289 var quantity = this.$quantityInput.length ? this.$quantityInput.val() : 1;
4290
4291 this.$cartPopupTitle.text(item.product_title);
4292 this.$cartPopupQuantity.text(quantity);
4293 this.$cartPopupQuantityLabel.text(
4294 theme.strings.quantityLabel.replace('[count]', quantity)
4295 );
4296
4297 this._setCartPopupPlaceholder(
4298 item.featured_image.url,
4299 item.featured_image.aspect_ratio
4300 );
4301 this._setCartPopupImage(item.featured_image.url, item.featured_image.alt);
4302 this._setCartPopupProductDetails(
4303 item.product_has_only_default_variant,
4304 item.options_with_values,
4305 item.properties
4306 );
4307
4308 $.getJSON('/cart.js').then(
4309 function(cart) {
4310 this._setCartQuantity(cart.item_count);
4311 this._setCartCountBubble(cart.item_count);
4312 this._showCartPopup();
4313 }.bind(this)
4314 );
4315 },
4316
4317 _setupCartPopupEventListeners: function() {
4318 this.$cartPopupWrapper.on(
4319 'keyup',
4320 function(event) {
4321 if (event.keyCode === slate.utils.keyboardKeys.ESCAPE) {
4322 this._hideCartPopup(event);
4323 }
4324 }.bind(this)
4325 );
4326
4327 this.$cartPopupClose.on('click', this._hideCartPopup.bind(this));
4328 this.$cartPopupDismiss.on('click', this._hideCartPopup.bind(this));
4329 $('body').on('click', this._onBodyClick.bind(this));
4330 },
4331
4332 _setCartPopupPlaceholder: function(imageUrl, imageAspectRatio) {
4333 this.$cartPopupImageWrapper =
4334 this.$cartPopupImageWrapper || $(this.selectors.cartPopupImageWrapper);
4335
4336 if (imageUrl === null) {
4337 this.$cartPopupImageWrapper.addClass(this.classes.hidden);
4338 return;
4339 }
4340
4341 var $placeholder = $(this.selectors.cartPopupPlaceholderSize);
4342 var maxWidth = 95 * imageAspectRatio;
4343 var heightRatio = 100 / imageAspectRatio;
4344
4345 this.$cartPopupImagePlaceholder.css('max-width', maxWidth);
4346
4347 $placeholder.css('padding-top', heightRatio + '%');
4348 },
4349
4350 _setCartPopupImage: function(imageUrl, imageAlt) {
4351 if (imageUrl === null) return;
4352
4353 this.$cartPopupImageWrapper.removeClass(this.classes.hidden);
4354 var sizedImageUrl = theme.Images.getSizedImageUrl(imageUrl, '200x');
4355 var image = document.createElement('img');
4356 image.src = sizedImageUrl;
4357 image.alt = imageAlt;
4358 image.classList.add(this.classes.cartImage);
4359 image.dataset.cartPopupImage = '';
4360
4361 image.onload = function() {
4362 this.$cartPopupImagePlaceholder.addClass(this.classes.hidden);
4363 this.$cartPopupImageWrapper.append(image);
4364 }.bind(this);
4365 },
4366
4367 _setCartPopupProductDetails: function(
4368 product_has_only_default_variant,
4369 options,
4370 properties
4371 ) {
4372 this.$cartPopupProductDetails =
4373 this.$cartPopupProductDetails ||
4374 $(this.selectors.cartPopupProductDetails);
4375 var variantPropertiesHTML = '';
4376
4377 if (!product_has_only_default_variant) {
4378 variantPropertiesHTML =
4379 variantPropertiesHTML + this._getVariantOptionList(options);
4380 }
4381
4382 if (properties !== null && Object.keys(properties).length !== 0) {
4383 variantPropertiesHTML =
4384 variantPropertiesHTML + this._getPropertyList(properties);
4385 }
4386
4387 if (variantPropertiesHTML.length === 0) {
4388 this.$cartPopupProductDetails.html('');
4389 this.$cartPopupProductDetails.attr('hidden', '');
4390 } else {
4391 this.$cartPopupProductDetails.html(variantPropertiesHTML);
4392 this.$cartPopupProductDetails.removeAttr('hidden');
4393 }
4394 },
4395
4396 _getVariantOptionList: function(variantOptions) {
4397 var variantOptionListHTML = '';
4398
4399 variantOptions.forEach(function(variantOption) {
4400 variantOptionListHTML =
4401 variantOptionListHTML +
4402 '<li class="product-details__item product-details__item--variant-option">' +
4403 variantOption.name +
4404 ': ' +
4405 variantOption.value +
4406 '</li>';
4407 });
4408
4409 return variantOptionListHTML;
4410 },
4411
4412 _getPropertyList: function(properties) {
4413 var propertyListHTML = '';
4414 var propertiesArray = Object.entries(properties);
4415
4416 propertiesArray.forEach(function(property) {
4417 // Line item properties prefixed with an underscore are not to be displayed
4418 if (property[0].charAt(0) === '_') return;
4419
4420 // if the property value has a length of 0 (empty), don't display it
4421 if (property[1].length === 0) return;
4422
4423 propertyListHTML =
4424 propertyListHTML +
4425 '<li class="product-details__item product-details__item--property">' +
4426 '<span class="product-details__property-label">' +
4427 property[0] +
4428 ': </span>' +
4429 property[1];
4430 ': ' + '</li>';
4431 });
4432
4433 return propertyListHTML;
4434 },
4435
4436 _setCartQuantity: function(quantity) {
4437 this.$cartPopupCartQuantity =
4438 this.$cartPopupCartQuantity || $(this.selectors.cartPopupCartQuantity);
4439 var ariaLabel;
4440
4441 if (quantity === 1) {
4442 ariaLabel = theme.strings.oneCartCount;
4443 } else if (quantity > 1) {
4444 ariaLabel = theme.strings.otherCartCount.replace('[count]', quantity);
4445 }
4446
4447 this.$cartPopupCartQuantity.text(quantity).attr('aria-label', ariaLabel);
4448 },
4449
4450 _setCartCountBubble: function(quantity) {
4451 this.$cartCountBubble =
4452 this.$cartCountBubble || $(this.selectors.cartCountBubble);
4453 this.$cartCount = this.$cartCount || $(this.selectors.cartCount);
4454
4455 this.$cartCountBubble.removeClass(this.classes.hidden);
4456 this.$cartCount.text(quantity);
4457 },
4458
4459 _showCartPopup: function() {
4460 this.$cartPopupWrapper
4461 .prepareTransition()
4462 .removeClass(this.classes.cartPopupWrapperHidden);
4463 this._handleButtonLoadingState(false);
4464
4465 slate.a11y.trapFocus({
4466 $container: this.$cartPopupWrapper,
4467 $elementToFocus: this.$cartPopup,
4468 namespace: 'cartPopupFocus'
4469 });
4470 },
4471
4472 _hideCartPopup: function(event) {
4473 var setFocus = event.detail === 0 ? true : false;
4474 this.$cartPopupWrapper
4475 .prepareTransition()
4476 .addClass(this.classes.cartPopupWrapperHidden);
4477
4478 $(this.selectors.cartPopupImage).remove();
4479 this.$cartPopupImagePlaceholder.removeClass(this.classes.hidden);
4480
4481 slate.a11y.removeTrapFocus({
4482 $container: this.$cartPopupWrapper,
4483 namespace: 'cartPopupFocus'
4484 });
4485
4486 if (setFocus) this.$previouslyFocusedElement[0].focus();
4487
4488 this.$cartPopupWrapper.off('keyup');
4489 this.$cartPopupClose.off('click');
4490 this.$cartPopupDismiss.off('click');
4491 $('body').off('click');
4492 },
4493
4494 _onBodyClick: function(event) {
4495 var $target = $(event.target);
4496
4497 if (
4498 $target[0] !== this.$cartPopupWrapper[0] &&
4499 !$target.parents(this.selectors.cartPopup).length
4500 ) {
4501 this._hideCartPopup(event);
4502 }
4503 },
4504
4505 _setActiveThumbnail: function(imageId) {
4506 // If there is no element passed, find it by the current product image
4507 if (typeof imageId === 'undefined') {
4508 imageId = $(
4509 this.selectors.productImageWraps + ':not(.hide)',
4510 this.$container
4511 ).data('image-id');
4512 }
4513
4514 var $thumbnailWrappers = $(
4515 this.selectors.productThumbListItem + ':not(.slick-cloned)',
4516 this.$container
4517 );
4518
4519 var $activeThumbnail = $thumbnailWrappers.find(
4520 this.selectors.productThumbImages +
4521 "[data-thumbnail-id='" +
4522 imageId +
4523 "']"
4524 );
4525
4526 $(this.selectors.productThumbImages)
4527 .removeClass(this.classes.activeClass)
4528 .removeAttr('aria-current');
4529
4530 $activeThumbnail.addClass(this.classes.activeClass);
4531 $activeThumbnail.attr('aria-current', true);
4532
4533 if (!$thumbnailWrappers.hasClass('slick-slide')) {
4534 return;
4535 }
4536
4537 var slideIndex = $activeThumbnail.parent().data('slick-index');
4538
4539 $(this.selectors.productThumbs).slick('slickGoTo', slideIndex, true);
4540 },
4541
4542 _switchImage: function(imageId) {
4543 var $newImage = $(
4544 this.selectors.productImageWraps + "[data-image-id='" + imageId + "']",
4545 this.$container
4546 );
4547 var $otherImages = $(
4548 this.selectors.productImageWraps +
4549 ":not([data-image-id='" +
4550 imageId +
4551 "'])",
4552 this.$container
4553 );
4554
4555 $newImage.removeClass(this.classes.hidden);
4556 $otherImages.addClass(this.classes.hidden);
4557 },
4558
4559 _handleImageFocus: function(evt) {
4560 if (evt.keyCode !== slate.utils.keyboardKeys.ENTER) return;
4561
4562 $(this.selectors.productFeaturedImage + ':visible').focus();
4563 },
4564
4565 _initThumbnailSlider: function() {
4566 var options = {
4567 slidesToShow: 4,
4568 slidesToScroll: 3,
4569 infinite: false,
4570 prevArrow: '.thumbnails-slider__prev--' + this.settings.sectionId,
4571 nextArrow: '.thumbnails-slider__next--' + this.settings.sectionId,
4572 responsive: [
4573 {
4574 breakpoint: 321,
4575 settings: {
4576 slidesToShow: 3
4577 }
4578 }
4579 ]
4580 };
4581
4582 $(this.selectors.productThumbs).slick(options);
4583
4584 // Accessibility concerns not yet fixed in Slick Slider
4585 $(this.selectors.productThumbsWrapper, this.$container)
4586 .find('.slick-list')
4587 .removeAttr('aria-live');
4588 $(this.selectors.productThumbsWrapper, this.$container)
4589 .find('.slick-disabled')
4590 .removeAttr('aria-disabled');
4591
4592 this.settings.sliderActive = true;
4593 },
4594
4595 _destroyThumbnailSlider: function() {
4596 $(this.selectors.productThumbs).slick('unslick');
4597 this.settings.sliderActive = false;
4598
4599 // Accessibility concerns not yet fixed in Slick Slider
4600 $(this.selectors.productThumbsWrapper, this.$container)
4601 .find('[tabindex="-1"]')
4602 .removeAttr('tabindex');
4603 },
4604
4605 _liveRegionText: function(variant) {
4606 // Dummy content for live region
4607 var liveRegionText =
4608 '[Availability] [Regular] [$$] [Sale] [$]. [UnitPrice] [$$$]';
4609
4610 if (!variant) {
4611 liveRegionText = theme.strings.unavailable;
4612 return liveRegionText;
4613 }
4614
4615 // Update availability
4616 var availability = variant.available ? '' : theme.strings.soldOut + ',';
4617 liveRegionText = liveRegionText.replace('[Availability]', availability);
4618
4619 // Update pricing information
4620 var regularLabel = '';
4621 var regularPrice = theme.Currency.formatMoney(
4622 variant.price,
4623 theme.moneyFormat
4624 );
4625 var saleLabel = '';
4626 var salePrice = '';
4627 var unitLabel = '';
4628 var unitPrice = '';
4629
4630 if (variant.compare_at_price > variant.price) {
4631 regularLabel = theme.strings.regularPrice;
4632 regularPrice =
4633 theme.Currency.formatMoney(
4634 variant.compare_at_price,
4635 theme.moneyFormat
4636 ) + ',';
4637 saleLabel = theme.strings.sale;
4638 salePrice = theme.Currency.formatMoney(
4639 variant.price,
4640 theme.moneyFormat
4641 );
4642 }
4643
4644 if (variant.unit_price) {
4645 unitLabel = theme.strings.unitPrice;
4646 unitPrice =
4647 theme.Currency.formatMoney(variant.unit_price, theme.moneyFormat) +
4648 ' ' +
4649 theme.strings.unitPriceSeparator +
4650 ' ' +
4651 this._getBaseUnit(variant);
4652 }
4653
4654 liveRegionText = liveRegionText
4655 .replace('[Regular]', regularLabel)
4656 .replace('[$$]', regularPrice)
4657 .replace('[Sale]', saleLabel)
4658 .replace('[$]', salePrice)
4659 .replace('[UnitPrice]', unitLabel)
4660 .replace('[$$$]', unitPrice)
4661 .trim();
4662
4663 return liveRegionText;
4664 },
4665
4666 _updateLiveRegion: function(evt) {
4667 var variant = evt.variant;
4668 var liveRegion = this.container.querySelector(
4669 this.selectors.productStatus
4670 );
4671 liveRegion.innerHTML = this._liveRegionText(variant);
4672 liveRegion.setAttribute('aria-hidden', false);
4673
4674 // hide content from accessibility tree after announcement
4675 setTimeout(function() {
4676 liveRegion.setAttribute('aria-hidden', true);
4677 }, 1000);
4678 },
4679
4680 _updateAddToCart: function(evt) {
4681 var variant = evt.variant;
4682
4683 if (variant) {
4684 if (variant.available) {
4685 this.$addToCart
4686 .removeAttr('aria-disabled')
4687 .attr('aria-label', theme.strings.addToCart);
4688 $(this.selectors.addToCartText, this.$container).text(
4689 theme.strings.addToCart
4690 );
4691 this.$shopifyPaymentButton.show();
4692 } else {
4693 // The variant doesn't exist, disable submit button and change the text.
4694 // This may be an error or notice that a specific variant is not available.
4695 this.$addToCart
4696 .attr('aria-disabled', true)
4697 .attr('aria-label', theme.strings.soldOut);
4698 $(this.selectors.addToCartText, this.$container).text(
4699 theme.strings.soldOut
4700 );
4701 this.$shopifyPaymentButton.hide();
4702 }
4703 } else {
4704 this.$addToCart
4705 .attr('aria-disabled', true)
4706 .attr('aria-label', theme.strings.unavailable);
4707 $(this.selectors.addToCartText, this.$container).text(
4708 theme.strings.unavailable
4709 );
4710 this.$shopifyPaymentButton.hide();
4711 }
4712 },
4713
4714 _updateAvailability: function(evt) {
4715 // remove error message if one is showing
4716 this._hideErrorMessage();
4717
4718 // update form submit
4719 this._updateAddToCart(evt);
4720 // update live region
4721 this._updateLiveRegion(evt);
4722
4723 this._updatePrice(evt);
4724 },
4725
4726 _updateImages: function(evt) {
4727 var variant = evt.variant;
4728 var imageId = variant.featured_image.id;
4729
4730 this._switchImage(imageId);
4731 this._setActiveThumbnail(imageId);
4732 },
4733
4734 _updatePrice: function(evt) {
4735 var variant = evt.variant;
4736
4737 var $priceContainer = $(this.selectors.priceContainer, this.$container);
4738 var $regularPrice = $(this.selectors.regularPrice, $priceContainer);
4739 var $salePrice = $(this.selectors.salePrice, $priceContainer);
4740 var $unitPrice = $(this.selectors.unitPrice, $priceContainer);
4741 var $unitPriceBaseUnit = $(
4742 this.selectors.unitPriceBaseUnit,
4743 $priceContainer
4744 );
4745
4746 // Reset product price state
4747 $priceContainer
4748 .removeClass(this.classes.productUnavailable)
4749 .removeClass(this.classes.productOnSale)
4750 .removeClass(this.classes.productUnitAvailable)
4751 .removeClass(this.classes.productSoldOut)
4752 .removeAttr('aria-hidden');
4753
4754 this.$productPolicies.removeClass(this.classes.visibilityHidden);
4755
4756 // Unavailable
4757 if (!variant) {
4758 $priceContainer
4759 .addClass(this.classes.productUnavailable)
4760 .attr('aria-hidden', true);
4761
4762 this.$productPolicies.addClass(this.classes.visibilityHidden);
4763 return;
4764 }
4765
4766 // Sold out
4767 if (!variant.available) {
4768 $priceContainer.addClass(this.classes.productSoldOut);
4769 return;
4770 }
4771
4772 // On sale
4773 if (variant.compare_at_price > variant.price) {
4774 $regularPrice.html(
4775 theme.Currency.formatMoney(
4776 variant.compare_at_price,
4777 theme.moneyFormat
4778 )
4779 );
4780 $salePrice.html(
4781 theme.Currency.formatMoney(variant.price, theme.moneyFormat)
4782 );
4783 $priceContainer.addClass(this.classes.productOnSale);
4784 } else {
4785 // Regular price
4786 $regularPrice.html(
4787 theme.Currency.formatMoney(variant.price, theme.moneyFormat)
4788 );
4789 }
4790
4791 // Unit price
4792 if (variant.unit_price) {
4793 $unitPrice.html(
4794 theme.Currency.formatMoney(variant.unit_price, theme.moneyFormat)
4795 );
4796 $unitPriceBaseUnit.html(this._getBaseUnit(variant));
4797 $priceContainer.addClass(this.classes.productUnitAvailable);
4798 }
4799 },
4800
4801 _getBaseUnit: function(variant) {
4802 return variant.unit_price_measurement.reference_value === 1
4803 ? variant.unit_price_measurement.reference_unit
4804 : variant.unit_price_measurement.reference_value +
4805 variant.unit_price_measurement.reference_unit;
4806 },
4807
4808 _updateSKU: function(evt) {
4809 var variant = evt.variant;
4810
4811 // Update the sku
4812 $(this.selectors.SKU).html(variant.sku);
4813 },
4814
4815 onUnload: function() {
4816 this.$container.off(this.settings.namespace);
4817 }
4818 });
4819
4820 function _enableZoom(el) {
4821 var zoomUrl = $(el).data('zoom');
4822 $(el).zoom({
4823 url: zoomUrl
4824 });
4825 }
4826
4827 function _destroyZoom(el) {
4828 $(el).trigger('zoom.destroy');
4829
4830 }
4831 return Product;
4832})();
4833
4834theme.ProductRecommendations = (function() {
4835 function ProductRecommendations(container) {
4836 this.$container = $(container);
4837
4838 var baseUrl = this.$container.data('baseUrl');
4839 var productId = this.$container.data('productId');
4840 var recommendationsSectionUrl =
4841 baseUrl +
4842 '?section_id=product-recommendations&product_id=' +
4843 productId +
4844 '&limit=4';
4845
4846 $.get(recommendationsSectionUrl).then(
4847 function(section) {
4848 var recommendationsMarkup = $(section).html();
4849 if (recommendationsMarkup.trim() !== '') {
4850 this.$container.html(recommendationsMarkup);
4851 }
4852 }.bind(this)
4853 );
4854 }
4855
4856 return ProductRecommendations;
4857})();
4858
4859theme.Quotes = (function() {
4860 var config = {
4861 mediaQuerySmall: 'screen and (max-width: 749px)',
4862 mediaQueryMediumUp: 'screen and (min-width: 750px)',
4863 slideCount: 0
4864 };
4865 var defaults = {
4866 accessibility: true,
4867 arrows: false,
4868 dots: true,
4869 autoplay: false,
4870 touchThreshold: 20,
4871 slidesToShow: 3,
4872 slidesToScroll: 3
4873 };
4874
4875 function Quotes(container) {
4876 var $container = (this.$container = $(container));
4877 var sectionId = $container.attr('data-section-id');
4878 var wrapper = (this.wrapper = '.quotes-wrapper');
4879 var slider = (this.slider = '#Quotes-' + sectionId);
4880 var $slider = $(slider, wrapper);
4881
4882 var sliderActive = false;
4883 var mobileOptions = $.extend({}, defaults, {
4884 slidesToShow: 1,
4885 slidesToScroll: 1,
4886 adaptiveHeight: true
4887 });
4888
4889 config.slideCount = $slider.data('count');
4890
4891 // Override slidesToShow/Scroll if there are not enough blocks
4892 if (config.slideCount < defaults.slidesToShow) {
4893 defaults.slidesToShow = config.slideCount;
4894 defaults.slidesToScroll = config.slideCount;
4895 }
4896
4897 $slider.on('init', this.a11y.bind(this));
4898
4899 enquire.register(config.mediaQuerySmall, {
4900 match: function() {
4901 initSlider($slider, mobileOptions);
4902 }
4903 });
4904
4905 enquire.register(config.mediaQueryMediumUp, {
4906 match: function() {
4907 initSlider($slider, defaults);
4908 }
4909 });
4910
4911 function initSlider(sliderObj, args) {
4912 if (sliderActive) {
4913 sliderObj.slick('unslick');
4914 sliderActive = false;
4915 }
4916
4917 sliderObj.slick(args);
4918 sliderActive = true;
4919 }
4920 }
4921
4922 Quotes.prototype = _.assignIn({}, Quotes.prototype, {
4923 onUnload: function() {
4924 enquire.unregister(config.mediaQuerySmall);
4925 enquire.unregister(config.mediaQueryMediumUp);
4926
4927 $(this.slider, this.wrapper).slick('unslick');
4928 },
4929
4930 onBlockSelect: function(evt) {
4931 // Ignore the cloned version
4932 var $slide = $(
4933 '.quotes-slide--' + evt.detail.blockId + ':not(.slick-cloned)'
4934 );
4935 var slideIndex = $slide.data('slick-index');
4936
4937 // Go to selected slide, pause autoplay
4938 $(this.slider, this.wrapper).slick('slickGoTo', slideIndex);
4939 },
4940
4941 a11y: function(event, obj) {
4942 var $list = obj.$list;
4943 var $wrapper = $(this.wrapper, this.$container);
4944
4945 // Remove default Slick aria-live attr until slider is focused
4946 $list.removeAttr('aria-live');
4947
4948 // When an element in the slider is focused set aria-live
4949 $wrapper.on('focusin', function(evt) {
4950 if ($wrapper.has(evt.target).length) {
4951 $list.attr('aria-live', 'polite');
4952 }
4953 });
4954
4955 // Remove aria-live
4956 $wrapper.on('focusout', function(evt) {
4957 if ($wrapper.has(evt.target).length) {
4958 $list.removeAttr('aria-live');
4959 }
4960 });
4961 }
4962 });
4963
4964 return Quotes;
4965})();
4966
4967theme.slideshows = {};
4968
4969theme.SlideshowSection = (function() {
4970 function SlideshowSection(container) {
4971 var $container = (this.$container = $(container));
4972 var sectionId = $container.attr('data-section-id');
4973 var slideshow = (this.slideshow = '#Slideshow-' + sectionId);
4974
4975 theme.slideshows[slideshow] = new theme.Slideshow(slideshow, sectionId);
4976 }
4977
4978 return SlideshowSection;
4979})();
4980
4981theme.SlideshowSection.prototype = _.assignIn(
4982 {},
4983 theme.SlideshowSection.prototype,
4984 {
4985 onUnload: function() {
4986 delete theme.slideshows[this.slideshow];
4987 },
4988
4989 onBlockSelect: function(evt) {
4990 var $slideshow = $(this.slideshow);
4991 var adaptHeight = $slideshow.data('adapt-height');
4992
4993 if (adaptHeight) {
4994 theme.slideshows[this.slideshow].setSlideshowHeight();
4995 }
4996
4997 // Ignore the cloned version
4998 var $slide = $(
4999 '.slideshow__slide--' + evt.detail.blockId + ':not(.slick-cloned)'
5000 );
5001 var slideIndex = $slide.data('slick-index');
5002
5003 // Go to selected slide, pause auto-rotate
5004 $slideshow.slick('slickGoTo', slideIndex).slick('slickPause');
5005 },
5006
5007 onBlockDeselect: function() {
5008 // Resume auto-rotate
5009 $(this.slideshow).slick('slickPlay');
5010 }
5011 }
5012);
5013
5014theme.slideshows = {};
5015
5016theme.VideoSection = (function() {
5017 function VideoSection(container) {
5018 var $container = (this.$container = $(container));
5019
5020 $('.video', $container).each(function() {
5021 var $el = $(this);
5022 theme.Video.init($el);
5023 theme.Video.editorLoadVideo($el.attr('id'));
5024 });
5025 }
5026
5027 return VideoSection;
5028})();
5029
5030theme.VideoSection.prototype = _.assignIn({}, theme.VideoSection.prototype, {
5031 onUnload: function() {
5032 theme.Video.removeEvents();
5033 }
5034});
5035
5036theme.heros = {};
5037
5038theme.HeroSection = (function() {
5039 function HeroSection(container) {
5040 var $container = (this.$container = $(container));
5041 var sectionId = $container.attr('data-section-id');
5042 var hero = '#Hero-' + sectionId;
5043 theme.heros[hero] = new theme.Hero(hero, sectionId);
5044 }
5045
5046 return HeroSection;
5047})();
5048
5049
5050$(document).ready(function() {
5051 var sections = new theme.Sections();
5052
5053 sections.register('cart-template', theme.Cart);
5054 sections.register('product', theme.Product);
5055 sections.register('collection-template', theme.Filters);
5056 sections.register('product-template', theme.Product);
5057 sections.register('header-section', theme.HeaderSection);
5058 sections.register('map', theme.Maps);
5059 sections.register('slideshow-section', theme.SlideshowSection);
5060 sections.register('video-section', theme.VideoSection);
5061 sections.register('quotes', theme.Quotes);
5062 sections.register('hero-section', theme.HeroSection);
5063 sections.register('product-recommendations', theme.ProductRecommendations);
5064});
5065
5066theme.init = function() {
5067 theme.customerTemplates.init();
5068
5069 // Theme-specific selectors to make tables scrollable
5070 var tableSelectors = '.rte table,' + '.custom__item-inner--html table';
5071
5072 slate.rte.wrapTable({
5073 $tables: $(tableSelectors),
5074 tableWrapperClass: 'scrollable-wrapper'
5075 });
5076
5077 // Theme-specific selectors to make iframes responsive
5078 var iframeSelectors =
5079 '.rte iframe[src*="youtube.com/embed"],' +
5080 '.rte iframe[src*="player.vimeo"],' +
5081 '.custom__item-inner--html iframe[src*="youtube.com/embed"],' +
5082 '.custom__item-inner--html iframe[src*="player.vimeo"]';
5083
5084 slate.rte.wrapIframe({
5085 $iframes: $(iframeSelectors),
5086 iframeWrapperClass: 'video-wrapper'
5087 });
5088
5089 // Common a11y fixes
5090 slate.a11y.pageLinkFocus($(window.location.hash));
5091
5092 $('.in-page-link').on('click', function(evt) {
5093 slate.a11y.pageLinkFocus($(evt.currentTarget.hash));
5094 });
5095
5096 $('a[href="#"]').on('click', function(evt) {
5097 evt.preventDefault();
5098 });
5099
5100 slate.a11y.accessibleLinks({
5101 messages: {
5102 newWindow: theme.strings.newWindow,
5103 external: theme.strings.external,
5104 newWindowExternal: theme.strings.newWindowExternal
5105 },
5106 $links: $('a[href]:not([aria-describedby], .product-single__thumbnail)')
5107 });
5108
5109 theme.FormStatus.init();
5110
5111 var selectors = {
5112 image: '[data-image]',
5113 imagePlaceholder: '[data-image-placeholder]',
5114 imageWithPlaceholderWrapper: '[data-image-with-placeholder-wrapper]',
5115 lazyloaded: '.lazyloaded'
5116 };
5117
5118 var classes = {
5119 hidden: 'hide'
5120 };
5121
5122 $(document).on('lazyloaded', function(e) {
5123 var $target = $(e.target);
5124
5125 if ($target.data('bgset')) {
5126 var $image = $target.find(selectors.lazyloaded);
5127 if ($image) {
5128 if ($target.data('bg')) {
5129 $image.attr('src', $target.data('bg'));
5130 }
5131 if ($target.data('alt')) {
5132 $image.attr('alt', $target.data('alt'));
5133 }
5134 }
5135 }
5136
5137 if (!$target.is(selectors.image)) {
5138 return;
5139 }
5140
5141 $target
5142 .closest(selectors.imageWithPlaceholderWrapper)
5143 .find(selectors.imagePlaceholder)
5144 .addClass(classes.hidden);
5145 });
5146
5147 // When the theme loads, lazysizes might load images before the "lazyloaded"
5148 // event listener has been attached. When this happens, the following function
5149 // hides the loading placeholders.
5150 function onLoadHideLazysizesAnimation() {
5151 $(selectors.image + '.lazyloaded')
5152 .closest(selectors.imageWithPlaceholderWrapper)
5153 .find(selectors.imagePlaceholder)
5154 .addClass(classes.hidden);
5155 }
5156
5157 onLoadHideLazysizesAnimation();
5158};
5159
5160$(theme.init);
5161
5162
5163$('iframe').load( function() {
5164 $('iframe').contents().find("head")
5165 .append($("<style type='text/css'> .branding{display:none;} </style>"));
5166});
5167
5168
5169
5170/* custom js om checkbox voor voorwaarden toe te voegen AZ */
5171$(document).ready(function() {
5172 $('body').on('click', '[name="checkout"], [name="goto_pp"], [name="goto_gc"]', function() {
5173 if ($('#agree').is(':checked')) {
5174 $(this).submit();
5175 }
5176 else {
5177 alert("Ben je akkoord met de Algemene Voorwaarden? Kruis het vinkje dan nog even aan!");
5178 return false;
5179 }
5180 });
5181 });