· 5 years ago · Dec 01, 2020, 04:04 PM
1/*
2* This is an unminified version of the theme.min.js file used by your theme.
3* If you want to use this file, you will need to change the script reference in your theme
4* Change <script src="{{ 'theme.min.js' | asset_url }}"> to:
5* <script src="{{ 'theme.js' | asset_url }}">
6*/
7(function ($,omit,find,throttle,a11y,remove,filter,fill,debounce,morphdom,defaultTo,findIndex,isArray,Hammer) {
8$ = 'default' in $ ? $['default'] : $;
9omit = 'default' in omit ? omit['default'] : omit;
10find = 'default' in find ? find['default'] : find;
11throttle = 'default' in throttle ? throttle['default'] : throttle;
12remove = 'default' in remove ? remove['default'] : remove;
13filter = 'default' in filter ? filter['default'] : filter;
14fill = 'default' in fill ? fill['default'] : fill;
15debounce = 'default' in debounce ? debounce['default'] : debounce;
16morphdom = 'default' in morphdom ? morphdom['default'] : morphdom;
17defaultTo = 'default' in defaultTo ? defaultTo['default'] : defaultTo;
18findIndex = 'default' in findIndex ? findIndex['default'] : findIndex;
19isArray = 'default' in isArray ? isArray['default'] : isArray;
20Hammer = 'default' in Hammer ? Hammer['default'] : Hammer;
21
22function Templates() {
23 this.registered = {};
24 this.instances = [];
25 this.extensions = {
26 '*': []
27 };
28}
29
30Templates.prototype = {
31 register: function(name, classname, properties) {
32 function Template(container, extensions) {
33 this.name = name.toLowerCase();
34 this.container = container;
35 this.$container = $(container);
36 this.extensions = extensions;
37 Master.call(this);
38 }
39
40 Template.classname = classname;
41 Template.constructor = Template;
42 Template.prototype = Object.create(Master.prototype);
43 $.extend(Template.prototype, properties);
44
45 this.registered[name] = Template;
46 },
47
48 extend: function(names, extension) {
49 if (names === '*') {
50 names = Object.keys(this.registered);
51 names.push('*');
52 } else if (typeof names === 'string') {
53 names = [names];
54 }
55
56 names.forEach(
57 function(name) {
58 this.extensions[name] = this.extensions[name] || [];
59 this.extensions[name].push(extension);
60
61 if (typeof this.registered[name] === 'undefined' || name === '*')
62 return;
63
64 this.instances.forEach(function(instance) {
65 if (instance.name !== name) return;
66
67 instance.extend(extension);
68 });
69 }.bind(this)
70 );
71 },
72
73 load: function(names) {
74 if (names === '*') {
75 names = Object.keys(this.registered);
76 } else if (typeof names === 'string') {
77 names = [names];
78 }
79
80 names.forEach(this._loadTemplate.bind(this));
81 },
82
83 _loadTemplate: function(name) {
84 var Template = this.registered[name];
85 var instance = find(this.instances, { name: name });
86 var container;
87 var extensions;
88
89 // If the template name is not registered or already has an instance loaded,
90 // then return
91 if (typeof Template === 'undefined' || instance) return;
92
93 // Get the container for the template
94 if (Template.classname === '*') {
95 container = document.body;
96 } else {
97 container = document.querySelector('body.' + Template.classname);
98 }
99
100 // If we don't have the specified container on the page then return
101 if (!container) return;
102
103 // Get all extensions for the new template instance
104 extensions = this.extensions['*'].concat(this.extensions[name] || []);
105 instance = new Template(container, extensions);
106
107 // Call the onLoad function of the template if it exists
108 if ($.isFunction(instance.onLoad)) {
109 instance.onLoad(container);
110 }
111
112 // Push the template instance to storage
113 this.instances.push(instance);
114 }
115};
116
117function Master() {
118 this.extensions.forEach(
119 function(extension) {
120 this.extend(extension);
121 }.bind(this)
122 );
123}
124
125Master.prototype = {
126 extend: function(extension) {
127 var init = extension.init;
128 this.extensions.push(extension);
129
130 if ($.isFunction(init)) {
131 extension = omit(extension, 'init');
132 }
133
134 $.extend(this, extension);
135 init.apply(this);
136 }
137};
138
139var templates = new Templates();
140
141var classes$1 = {
142 disableScroll: 'disable-scroll'
143};
144
145var utils = {
146 pointerEventToXY: function(event) {
147 var position;
148
149 if (event.originalEvent.touches) {
150 position =
151 event.originalEvent.touches[0] || event.originalEvent.changedTouches[0];
152 } else {
153 position = event;
154 }
155 return {
156 x: position.pageX,
157 y: position.pageY
158 };
159 },
160
161 promiseRepeatSeries: function(promise, count) {
162 count--;
163
164 return promise().then(
165 function() {
166 // eslint-disable-line consistent-return
167 if (count) {
168 return this.promiseRepeatSeries(promise, count);
169 }
170 }.bind(this)
171 );
172 },
173
174 mapPromiseSeries: function(iterable, cb, delay) {
175 var series = $.Deferred().resolve();
176
177 $.each(iterable, function(index, item) {
178 series = series.then(function() {
179 if (typeof delay === 'number') {
180 return delayedReturn(index, item);
181 } else {
182 return cb(index, item);
183 }
184 });
185 });
186
187 function delayedReturn(index, item) {
188 return $.Deferred(function(defer) {
189 setTimeout(function() {
190 defer.resolve(cb(index, item)); // eslint-disable-line callback-return
191 }, delay);
192 });
193 }
194
195 return series;
196 },
197
198 promiseAnimationEnd: function($el) {
199 var events = 'animationend webkitAnimationEnd oAnimationEnd';
200 var properties = [
201 'animation-duration',
202 '-moz-animation-duration',
203 '-webkit-animation-duration',
204 '-o-animation-duration'
205 ];
206 var duration = 0;
207 var promise = $.Deferred().resolve();
208
209 // check the various CSS properties to see if a duration has been set
210 $.each(properties, function(index, value) {
211 duration || (duration = parseFloat($el.css(value))); // eslint-disable-line no-unused-expressions
212 });
213
214 if (duration > 0) {
215 promise = $.Deferred(function(defer) {
216 $el.on(events, function(evt) {
217 if (evt.target !== $el[0]) return;
218 $el.off(events);
219 defer.resolve();
220 });
221 });
222 }
223
224 return promise;
225 },
226
227 promiseTransitionEnd: function($el) {
228 var events =
229 'webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend';
230 var properties = [
231 'transition-duration',
232 '-moz-transition-duration',
233 '-webkit-transition-duration',
234 '-o-transition-duration'
235 ];
236 var duration = 0;
237 var promise = $.Deferred().resolve();
238
239 // check the various CSS properties to see if a duration has been set
240 $.each(properties, function(index, value) {
241 duration || (duration = parseFloat($el.css(value))); // eslint-disable-line no-unused-expressions
242 });
243
244 if (duration > 0) {
245 promise = $.Deferred(function(defer) {
246 $el.on(events, function(evt) {
247 if (evt.target !== $el[0]) return;
248 $el.off(events);
249 defer.resolve();
250 });
251 });
252 }
253
254 return promise;
255 },
256
257 isLocalStorageSupported: function() {
258 var mod = 'localStorageTest';
259 try {
260 localStorage.setItem(mod, mod);
261 localStorage.removeItem(mod);
262 return true;
263 } catch (error) {
264 return false;
265 }
266 },
267
268 isSessionStorageSupported: function() {
269 var mod = 'sessionStorageTest';
270 try {
271 sessionStorage.setItem(mod, mod);
272 sessionStorage.removeItem(mod);
273 return true;
274 } catch (error) {
275 return false;
276 }
277 },
278
279 /**
280 * Set all tabbable elements tabindex attribute to '-1' so that they are not
281 * tabbable. Great for if you want to hide something with opacity: 0 or
282 * visibility: hidden, but don't want interactivity
283 *
284 * @param {any} container - Container to look for children elements to disable
285 */
286 disableTabbingOfChildren: function(containers) {
287 $(containers).each(function(index, container) {
288 $(
289 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]',
290 container
291 ).attr('tabindex', '-1');
292 });
293 },
294
295 /**
296 * Remove all tabbable elements' tabindex attribute so that they assume their
297 * normal tabbing behaviour.
298 *
299 * @param {any} container - Container to look for children elements to reset
300 */
301 enableTabbingOfChildren: function(containers) {
302 $(containers).each(function(index, container) {
303 $(
304 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]',
305 container
306 ).removeAttr('tabindex');
307 });
308 },
309
310 isiOSSafari: function() {
311 var userAgent = window.navigator.userAgent;
312 var iOS =
313 Boolean(userAgent.match(/iPad/i)) || Boolean(userAgent.match(/iPhone/i));
314 var webkit = Boolean(userAgent.match(/WebKit/i));
315
316 return iOS && webkit && !userAgent.match(/CriOS/i);
317 },
318
319 disableScrollBody: function() {
320 // If the body isn't tall enough to scroll then we have nothing to disable
321 if (document.body.scrollHeight - window.innerHeight === 0) return;
322
323 $('html').addClass(classes$1.disableScroll);
324 },
325
326 enableScrollBody: function() {
327 $('html').removeClass(classes$1.disableScroll);
328 },
329
330 updateUrlParameter: function(url, key, value) {
331 var re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i');
332 var separator = url.indexOf('?') === -1 ? '?' : '&';
333
334 if (url.match(re)) {
335 return url.replace(re, '$1' + key + '=' + value + '$2');
336 } else {
337 return url + separator + key + '=' + value;
338 }
339 },
340
341 cookiesEnabled: function() {
342 var cookieEnabled = navigator.cookieEnabled;
343
344 if (!cookieEnabled) {
345 document.cookie = 'testcookie';
346 cookieEnabled = document.cookie.indexOf('testcookie') !== -1;
347 }
348 return cookieEnabled;
349 },
350
351 onFirst: function(name, fn) {
352 // bind as you normally would
353 // don't want to miss out on any jQuery magic
354 this.on(name, fn);
355
356 this.each(function() {
357 var handlers = $._data(this, 'events')[name.split('.')[0]];
358 // take out the handler we just inserted from the end
359 var handler = handlers.pop();
360 // move it at the beginning
361 handlers.splice(0, 0, handler);
362 });
363 },
364
365 // Returns a promise that is only resolved when the main stylesheet has been
366 // downloaded.
367 promiseStylesheet: function() {
368 if (typeof this.stylesheetPromise === 'undefined') {
369 this.stylesheetPromise = $.Deferred(function(defer) {
370 var link = document.querySelector(
371 'link[href="' + theme.stylesheet + '"]'
372 );
373
374 if (link.loaded) {
375 defer.resolve();
376 }
377
378 /* eslint-disable no-undef */
379 onloadCSS(link, function() {
380 // Global onloadCSS function injected by load-css.liquid
381 defer.resolve();
382 });
383 });
384 }
385
386 return this.stylesheetPromise;
387 },
388
389 isMobile: function() {
390 // Create a scoped variable that stores the current status
391 var status = false;
392
393 // Replace utils.isMobile function with a function that returns the status.
394 // This makes sure the code below is on executed once, and then from there
395 // on the only thing that isMobile does is return var status.
396 this.isMobile = function() {
397 return status;
398 };
399
400 function checkIfMobile() {
401 status = $(window).outerWidth() < theme.mediaQuerySmall;
402 return status;
403 }
404
405 // Make sure that this resize handler is the first handler in the event loop
406 $.fn.onFirst = this.onFirst;
407
408 // This event handler is only assigned once.
409 $(window).onFirst('resize', checkIfMobile);
410
411 // Set the initial value of status and return that value. After this,
412 // isMobile() will return `var status`
413 return checkIfMobile();
414 },
415
416 isTablet: function() {
417 // Create a scoped variable that stores the current status
418 var status = false;
419
420 // Replace utils.isTablet function with a function that returns the status.
421 // This makes sure the code below is on executed once, and then from there
422 // on the only thing that isTablet does is return var status.
423 this.isTablet = function() {
424 return status;
425 };
426
427 function checkIfTablet() {
428 var width = $(window).outerWidth();
429 status = width > theme.mediaQuerySmall && width < theme.mediaQueryMedium;
430 return status;
431 }
432
433 // Make sure that this resize handler is the first handler in the event loop
434 $.fn.onFirst = this.onFirst;
435
436 // This event handler is only assigned once.
437 $(window).onFirst('resize', checkIfTablet);
438
439 // Set the initial value of status and return that value. After this,
440 // isTablet() will return `var status`
441 return checkIfTablet();
442 },
443
444 isInOrAboveViewport: function(element) {
445 var rect = element.getBoundingClientRect();
446 var offset = 50;
447
448 return (
449 // The top is in view: the top is more than 0 and less than the window height (the top of the element is in view)
450 (rect.top + offset >= 0 && rect.top + offset <= window.innerHeight) ||
451 // The bottom is in view: bottom position is greater than 0 and greater than the window height
452 (rect.bottom + offset >= 0 &&
453 rect.bottom + offset <= window.innerHeight) ||
454 // The top is above the viewport and the bottom is below the viewport
455 (rect.top + offset < 0 && rect.bottom + offset > window.innerHeight) ||
456 // The bottom is above the viewport
457 rect.bottom < 0
458 );
459 },
460
461 keyboardKeys: {
462 TAB: 9,
463 ENTER: 13,
464 SPACE: 32,
465 LEFTARROW: 37,
466 RIGHTARROW: 39
467 }
468};
469
470var selectors = {
471 elementsToAnimate: '[data-animate]'
472};
473
474var classes = {
475 animated: 'has-animated',
476 animationsDisabled: 'animations--disabled'
477};
478
479templates.register('Page animations', '*', {
480 onLoad: function() {
481 this.animationNamespace = '.animations';
482 this.$elementsToAnimate = $(selectors.elementsToAnimate, this.$container);
483
484 if (this.$elementsToAnimate.length === 0) return;
485
486 // This extension is used for templates and sections. Templates don't have
487 // event handling like sections so we need to use regular jQuery events
488 var scrollEvent = 'scroll' + this.animationNamespace;
489
490 if (Shopify.designMode) {
491 scrollEvent += ' touchmove' + this.animationNamespace;
492 }
493 $(window).on(
494 scrollEvent,
495 throttle(this._animateElementsInViewport.bind(this), 200)
496 );
497 $(window).on(
498 'resize' + this.animationNamespace,
499 throttle(this._animateElementsInViewport.bind(this), 200)
500 );
501
502 if (Shopify && Shopify.designMode) {
503 $(document).on(
504 'shopify:section:load shopify:section:select',
505 this._onSectionSelect.bind(this)
506 );
507 $(document).on(
508 'shopify:section:unload shopify:section:deselect',
509 this._onSectionDeselect.bind(this)
510 );
511 }
512
513 // Put this on a loop to check if elements on page load need to be animated
514 utils.promiseStylesheet().then(this._animateElementsInViewport.bind(this));
515 },
516
517 _animateElementsInViewport: function() {
518 this.$elementsToAnimate = this.$elementsToAnimate.map(function(
519 index,
520 element
521 ) {
522 // If the element is not going to be animated, return it to the list of
523 // elements to animate
524 if (!utils.isInOrAboveViewport(element)) {
525 return element;
526 }
527
528 // Debounce any style changes to the next frame.
529 requestAnimationFrame(function() {
530 $(element).addClass(classes.animated);
531
532 // This extension is also used for template, which don't have trigger and
533 // events functionality
534 $(element).trigger('animate_element', [element]);
535 });
536
537 // Since this element was animated, we return null so its NOT added back
538 // to the list of elements to animate
539 return null;
540 });
541
542 if (this.$elementsToAnimate.length === 0) {
543 $(window).off(this.animationNamespace);
544 }
545 },
546
547 _onSectionSelect: function() {
548 $(document.body).addClass(classes.animationsDisabled);
549 },
550
551 _onSectionDeselect: function(evt) {
552 $(document.body).removeClass(classes.animationsDisabled);
553
554 $(evt.target)
555 .find(selectors.elementsToAnimate)
556 .addClass(classes.animated);
557 }
558});
559
560var selectors$2 = {
561 socialSharing: '.social-sharing',
562 socialSharingToggle: '.social-sharing__toggle',
563 linkList: '.social-sharing__item-list'
564};
565var classes$3 = {
566 socialSharingAction: 'social-sharing--active'
567};
568
569var socialSharing = {
570 init: function() {
571 this.$elements = $(selectors$2.socialSharing, this.$container);
572 this.clicked = false;
573
574 $(selectors$2.socialSharingToggle, this.$container)
575 .on('click', this._onClick.bind(this))
576 .one('click', function() {
577 this.clicked = true;
578 });
579
580 this.$container.on('section_unload', this.destroySocialSharing.bind(this));
581 },
582
583 showSocialSharing: function() {
584 $(selectors$2.socialSharing, this.$container).addClass(
585 classes$3.socialSharingAction
586 );
587 $(selectors$2.socialSharingToggle, this.$container).attr(
588 'aria-expanded',
589 true
590 );
591 $(selectors$2.linkList, this.$container).attr('aria-hidden', false);
592 utils.enableTabbingOfChildren($(selectors$2.linkList, this.$container));
593 },
594
595 hideSocialSharing: function() {
596 $(selectors$2.socialSharing, this.$container).removeClass(
597 classes$3.socialSharingAction
598 );
599 $(selectors$2.socialSharingToggle, this.$container).attr(
600 'aria-expanded',
601 false
602 );
603 $(selectors$2.linkList, this.$container).attr('aria-hidden', true);
604 utils.disableTabbingOfChildren($(selectors$2.linkList, this.$container));
605 },
606
607 destroySocialSharing: function() {
608 $(selectors$2.socialSharingToggle, this.$container).off();
609 },
610
611 _onClick: function(evt) {
612 if ($(evt.currentTarget).attr('aria-expanded') === 'true') {
613 this.hideSocialSharing();
614 } else {
615 this.showSocialSharing();
616 }
617 }
618};
619
620var selectors$1 = {
621 articleContentWrapper: '.article__wrapper',
622 articleShareDesktop: '.article__share-desktop',
623 articleShareMobile: '.article__share-mobile',
624 articleShareDesktopWrapper: '.article__share-desktop-wrapper',
625 socialSharing: '.social-sharing'
626};
627
628var classes$2 = {
629 shareFixed: 'article__share-desktop--fixed',
630 shareBottom: 'article__share-desktop--bottom'
631};
632
633var settings = {
634 shareExpandedHeight: 350
635};
636
637templates.register('Article Template', 'template-article', {
638 onLoad: function() {
639 this.extend(socialSharing);
640
641 $(window)
642 .on('scroll', this.setSharePosition.bind(this))
643 .on('resize', this.updateMeasurements.bind(this));
644
645 utils.promiseStylesheet().then(
646 function() {
647 this.updateMeasurements();
648 }.bind(this)
649 );
650 },
651
652 setSharePosition: function() {
653 if (utils.isMobile() || utils.isTablet()) return;
654
655 var scrollTop = $(window).scrollTop();
656
657 // Debounce DOM edits to next frame with requestAnimationFrame
658 requestAnimationFrame(
659 function() {
660 if (scrollTop > this.fixedTop) {
661 if (scrollTop > this.fixedBottom) {
662 // Fix to bottom
663 $(selectors$1.articleShareDesktop)
664 .removeClass(classes$2.shareFixed)
665 .addClass(classes$2.shareBottom);
666 } else {
667 // Fix to top
668 $(selectors$1.articleShareDesktop)
669 .addClass(classes$2.shareFixed)
670 .removeClass(classes$2.shareBottom);
671 }
672 } else {
673 // Fix to side
674 $(selectors$1.articleShareDesktop)
675 .removeClass(classes$2.shareFixed)
676 .removeClass(classes$2.shareBottom);
677 }
678
679 if (!this.clicked) {
680 if (scrollTop > this.fixedBottom - settings.shareExpandedHeight) {
681 this.showSocialSharing();
682 } else {
683 this.hideSocialSharing();
684 }
685 }
686 }.bind(this)
687 );
688 },
689
690 updateMeasurements: function() {
691 this.fixedTop = $(selectors$1.articleContentWrapper).offset().top;
692 this.fixedBottom =
693 this.fixedTop +
694 $(selectors$1.articleContentWrapper).height() -
695 $(selectors$1.articleShareDesktopWrapper).height();
696 this.windowHeight = $(window).outerHeight();
697
698 this.setSharePosition();
699 }
700});
701
702var selectors$3 = {
703 addressCountrySelect: '.address-country-selector',
704 newToggle: '.address-new-toggle',
705 newAddressContainer: '.new-address',
706 editToggle: '.address-edit-toggle',
707 editAddressContainer: '.edit-address',
708 editAddressContainerId: '#EditAddress_',
709 deleteAddress: '.address-delete',
710 currentAddresses: '.current-addresses',
711 currentAddress: '#CurrentAddress_',
712 currentAddressesItem: '.current-addresses__item'
713};
714var classes$4 = {
715 newAddressHide: 'new-address--hidden',
716 editAddressShow: 'edit-address--show'
717};
718
719templates.register(
720 'Customer Addresses Template',
721 'template-customers-addresses',
722 {
723 onLoad: function(container) {
724 var $container = (this.$container = $(container));
725 var $countryOptions = $(theme.countryOptionTags).filter('option');
726 var $addressCountrySelect = $(selectors$3.addressCountrySelect, $container);
727 var $newAddressContainer = $(selectors$3.newAddressContainer, $container);
728 var $editAddressContainers = $(
729 selectors$3.editAddressContainer,
730 $container
731 );
732
733 $addressCountrySelect.each(
734 function(index, countrySelect) {
735 var $countrySelect = this._populateCountrySelect(
736 countrySelect,
737 $countryOptions
738 );
739 var currentProvinces = this._fetchCurrentProvinces($countrySelect);
740 var $provinceSelect = $(
741 $countrySelect.attr('data-province-select'),
742 $container
743 );
744 var defaultProvince = $provinceSelect.data('default');
745
746 this._populateProvinceSelect(
747 $provinceSelect,
748 currentProvinces,
749 defaultProvince
750 );
751 }.bind(this)
752 );
753
754 $(selectors$3.newToggle).on('click', this._onNewAddressClick.bind(this));
755 $(selectors$3.editToggle).on('click', this._onEditAddressClick.bind(this));
756 $(selectors$3.deleteAddress).on(
757 'click',
758 this._onDeleteAddressClick.bind(this)
759 );
760
761 if ($('div.errors', $newAddressContainer).length !== 0) {
762 $newAddressContainer.removeClass(classes$4.newAddressHide);
763 }
764
765 $editAddressContainers.each(function() {
766 if ($('div.errors', this).length !== 0) {
767 $(this).addClass(classes$4.editAddressShow);
768 }
769 });
770 },
771
772 _populateCountrySelect: function(countrySelect, countryOptions) {
773 var $countrySelect = $(countrySelect);
774 var defaultValue = $countrySelect.data('default');
775 var defaultOption = countryOptions[0];
776
777 // If the country <select> has a default value already set, that country
778 // value is written in the theme language (not always english). The <option>
779 // value attribute in the country <select> are always in English. We need to
780 // find the <option> that has the same .text value as our default value.
781 if (typeof defaultValue === 'string' && defaultValue !== '') {
782 defaultOption = find(countryOptions, function(el) {
783 return el.text === defaultValue;
784 });
785 }
786
787 return $countrySelect
788 .html(countryOptions.clone())
789 .val(defaultOption.value)
790 .on('change', this._onCountrySelectChange.bind(this));
791 },
792
793 _populateProvinceSelect: function(
794 $provinceSelect,
795 provinces,
796 defaultValue
797 ) {
798 var $container = $provinceSelect.parent();
799 var provinceOptions = provinces.map(this._createProvinceOption);
800 var value = defaultValue || $(provinceOptions[0]).val();
801
802 if (provinceOptions.length) {
803 $container.show();
804 } else {
805 $container.hide();
806 }
807
808 return $provinceSelect.html(provinceOptions).val(value);
809 },
810
811 _createProvinceOption: function(province) {
812 return $('<option>')
813 .attr({
814 value: province[1]
815 })
816 .text(province[1]);
817 },
818
819 _fetchCurrentProvinces: function($countrySelect) {
820 return $countrySelect.find(':selected').data('provinces') || [];
821 },
822
823 _onCountrySelectChange: function(event) {
824 var $countrySelect = $(event.target);
825 var currentProvinces = this._fetchCurrentProvinces($countrySelect);
826 var $provinceSelect = $($countrySelect.data('province-select'));
827
828 this._populateProvinceSelect($provinceSelect, currentProvinces);
829 },
830
831 _onNewAddressClick: function() {
832 $(selectors$3.newAddressContainer, this.$container).toggleClass(
833 classes$4.newAddressHide
834 );
835 },
836
837 _onEditAddressClick: function(event) {
838 var formId = $(event.target).data('form-id');
839 $(selectors$3.editAddressContainerId + formId, this.$container).toggleClass(
840 classes$4.editAddressShow
841 );
842 },
843
844 _onDeleteAddressClick: function(event) {
845 var $button = $(event.target);
846 var formId = $button.data('form-id');
847 var addressUrl = $button.data('address-url');
848 var confirmMessage = $button.data('confirm-message');
849 var $container = $(selectors$3.currentAddress + formId, this.$container);
850
851 if (confirm(confirmMessage)) {
852 // eslint-disable-line no-alert
853 $.post(addressUrl, {
854 _method: 'delete'
855 });
856 $container.remove();
857
858 if (!$(selectors$3.currentAddressesItem).length) {
859 $(selectors$3.currentAddresses, this.$container).remove();
860 $(selectors$3.newAddressContainer, this.$container).removeClass(
861 classes$4.newAddressHide
862 );
863 }
864 }
865 }
866 }
867);
868
869var selectors$4 = {
870 recoverPasswordForm: '#RecoverPasswordForm',
871 customerLoginForm: '#CustomerLoginForm',
872 hideRecoverPasswordLink: '#HideRecoverPasswordLink',
873 showRecoverPasswordLink: '#RecoverPassword'
874};
875
876templates.register('Customer Login Template', 'template-customers-login', {
877 onLoad: function(container) {
878 var $container = (this.$container = $(container));
879
880 this.checkUrlHash();
881 this.resetPasswordSuccess();
882
883 $(selectors$4.showRecoverPasswordLink, $container).on(
884 'click',
885 this.onShowHidePasswordForm.bind(this)
886 );
887 $(selectors$4.hideRecoverPasswordLink, $container).on(
888 'click',
889 this.onShowHidePasswordForm.bind(this)
890 );
891 },
892
893 onShowHidePasswordForm: function(evt) {
894 evt.preventDefault();
895 this.toggleRecoverPasswordForm();
896 },
897
898 checkUrlHash: function() {
899 var hash = window.location.hash;
900
901 if (hash === '#recover') {
902 this.toggleRecoverPasswordForm();
903 }
904 },
905
906 toggleRecoverPasswordForm: function() {
907 $(selectors$4.recoverPasswordForm).toggleClass('hide');
908 $(selectors$4.customerLoginForm).toggleClass('hide');
909 },
910
911 resetPasswordSuccess: function() {
912 if (!$('.reset-password-success').length) {
913 return;
914 }
915
916 $('#ResetSuccess').removeClass('hide');
917 }
918});
919
920if (!Element.prototype.matches) {
921 Element.prototype.matches =
922 Element.prototype.matchesSelector ||
923 Element.prototype.msMatchesSelector ||
924 Element.prototype.webkitMatchesSelector;
925}
926
927/**
928 * Rich Text Editor
929 * -----------------------------------------------------------------------------
930 * Wrap videos in div to force responsive layout.
931 *
932 * @namespace rte
933 */
934
935var rte = {
936 wrapTable: function() {
937 $('.rte table').wrap('<div class="rte__table-wrapper"></div>');
938 },
939
940 iframeReset: function() {
941 var $iframeVideo = $(
942 '.rte iframe[src*="youtube.com/embed"], .rte iframe[src*="player.vimeo"]'
943 );
944 var $iframeReset = $iframeVideo.add('.rte iframe#admin_bar_iframe');
945
946 $iframeVideo.each(function() {
947 // Add wrapper to make video responsive
948 $(this).wrap('<div class="video-wrapper"></div>');
949 });
950
951 $iframeReset.each(function() {
952 // Re-set the src attribute on each iframe after page load
953 // for Chrome's "incorrect iFrame content on 'back'" bug.
954 // https://code.google.com/p/chromium/issues/detail?id=395791
955 // Need to specifically target video and admin bar
956 this.src = this.src;
957 });
958 },
959
960 imageLink: function() {
961 // Add class to remove underline on image links.
962 $('.rte a:has(img)').addClass('image-link');
963 }
964};
965
966templates.register('Page Helpers', '*', {
967 onLoad: function() {
968 this.pageLinkFocus();
969 this.rteFixes();
970 this.disableOutlineOnClick();
971 this.checkIfMobileOS();
972 },
973
974 pageLinkFocus: function() {
975 // Common a11y fixes
976 a11y.focusHash({
977 className: 'js-focus-hidden'
978 });
979
980 a11y.bindInPageLinks({
981 className: 'js-focus-hidden',
982 ignore: '[data-link-no-focus]'
983 });
984 },
985
986 rteFixes: function() {
987 // Wrap videos in div to force responsive layout.
988 rte.wrapTable();
989 rte.iframeReset();
990 // Remove underline on image links.
991 rte.imageLink();
992 },
993
994 disableOutlineOnClick: function() {
995 // Disable focus outline when clicking, enable when navigating with keys
996 var classes = {
997 outlineDisabled: 'outline-disabled'
998 };
999
1000 $('html').addClass(classes.outlineDisabled);
1001
1002 $(document).on('keyup', function() {
1003 $('html').removeClass(classes.outlineDisabled);
1004 });
1005
1006 $(document).on('click', function(evt) {
1007 $('html').toggleClass(
1008 classes.outlineDisabled,
1009 !$(evt.target).is('input, textarea, select, button')
1010 );
1011 });
1012 },
1013
1014 checkIfMobileOS: function() {
1015 var ua = navigator.userAgent.toLowerCase();
1016 var isIOS = /ipad|iphone|ipod/.test(ua) && !window.MSStream;
1017 var isAndroid = /android/.test(ua);
1018
1019 if (isIOS) {
1020 $('html')
1021 .addClass('is-mobile-os')
1022 .addClass('is-ios');
1023 }
1024 if (isAndroid) {
1025 $('html')
1026 .addClass('is-mobile-os')
1027 .addClass('is-android');
1028 }
1029 }
1030});
1031
1032/**
1033 * A11y Helpers
1034 * -----------------------------------------------------------------------------
1035 * A collection of useful functions that help make your theme more accessible
1036 * to users with visual impairments.
1037 *
1038 *
1039 * @namespace a11y
1040 */
1041
1042var a11y$1 = {
1043 /**
1044 * For use when focus shifts to a container rather than a link
1045 * eg for In-page links, after scroll, focus shifts to content area so that
1046 * next `tab` is where user expects if focusing a link, just $link.focus();
1047 *
1048 * @param {JQuery} $element - The element to be acted upon
1049 */
1050 pageLinkFocus: function($element) {
1051 var focusClass = 'js-focus-hidden';
1052
1053 $element
1054 .first()
1055 .attr('tabIndex', '-1')
1056 .focus()
1057 .addClass(focusClass)
1058 .one('blur', callback);
1059
1060 function callback() {
1061 $element
1062 .first()
1063 .removeClass(focusClass)
1064 .removeAttr('tabindex');
1065 }
1066 },
1067
1068 /**
1069 * Attempts to focus an element, and if unsuccessful adds tabindex to the
1070 * element and focuses it. Tabindex is removed on element blur.
1071 *
1072 * @param {jQuery} $element - The element to be focused
1073 */
1074 forceFocus: function($element) {
1075 $element.focus();
1076
1077 if (!$element.is(document.activeElement)) {
1078 $element
1079 .attr('tabindex', '0')
1080 .focus()
1081 .one('blur', function() {
1082 $element.removeAttr('tabindex');
1083 });
1084 }
1085 },
1086
1087 /**
1088 * If there's a hash in the url, focus the appropriate element
1089 */
1090 focusHash: function() {
1091 var hash = window.location.hash;
1092
1093 // is there a hash in the url? is it an element on the page?
1094 if (hash && document.getElementById(hash.slice(1))) {
1095 this.pageLinkFocus($(hash));
1096 }
1097 },
1098
1099 /**
1100 * When an in-page (url w/hash) link is clicked, focus the appropriate element
1101 */
1102 bindInPageLinks: function() {
1103 $('a[href*=#]').on(
1104 'click',
1105 function(evt) {
1106 this.pageLinkFocus($(evt.currentTarget.hash));
1107 }.bind(this)
1108 );
1109 },
1110
1111 /**
1112 * Traps the focus in a particular container
1113 *
1114 * @param {object} options - Options to be used
1115 * @param {jQuery} options.$container - Container to trap focus within
1116 * @param {jQuery} options.$elementToFocus - Element to be focused when focus leaves container
1117 * @param {string} options.namespace - Namespace used for new focus event handler
1118 */
1119 trapFocus: function(options) {
1120 var eventName = options.namespace
1121 ? 'focusin.' + options.namespace
1122 : 'focusin';
1123
1124 if (!options.$elementToFocus) {
1125 options.$elementToFocus = options.$container;
1126 }
1127
1128 options.$container.attr('tabindex', '-1');
1129 options.$elementToFocus.focus();
1130
1131 $(document).on(eventName, function(evt) {
1132 if (
1133 options.$container[0] !== evt.target &&
1134 !options.$container.has(evt.target).length
1135 ) {
1136 options.$container.focus();
1137 }
1138 });
1139 },
1140
1141 /**
1142 * Removes the trap of focus in a particular container
1143 *
1144 * @param {object} options - Options to be used
1145 * @param {jQuery} options.$container - Container to trap focus within
1146 * @param {string} options.namespace - Namespace used for new focus event handler
1147 */
1148 removeTrapFocus: function(options) {
1149 var eventName = options.namespace
1150 ? 'focusin.' + options.namespace
1151 : 'focusin';
1152
1153 if (options.$container && options.$container.length) {
1154 options.$container.removeAttr('tabindex');
1155 }
1156
1157 $(document).off(eventName);
1158 }
1159};
1160
1161/**
1162 * prepareTransition
1163 * jQuery Plugin for ensuring transitions with display:none or visibility:hidden
1164 * are in the right state until the end of the transition
1165 *
1166 * Developed by Jonathan Snook (http://snook.ca/)
1167 * January 12, 2012
1168 *
1169 * Requires the following CSS:
1170 * .is-transitioning {
1171 * display: block !important;
1172 * visibility: visible !important;
1173 * }
1174 *
1175 * MIT license
1176 * http://www.opensource.org/licenses/mit-license.php
1177 */
1178
1179$.fn.prepareTransition = function(){
1180 return this.each(function(){
1181 var el = $(this);
1182 // remove the transition class upon completion
1183 el.one('TransitionEnd webkitTransitionEnd transitionend oTransitionEnd', function(){
1184 el.removeClass('is-transitioning');
1185 });
1186
1187 // check the various CSS properties to see if a duration has been set
1188 var cl = ["transition-duration", "-moz-transition-duration", "-webkit-transition-duration", "-o-transition-duration"];
1189 var duration = 0;
1190 $.each(cl, function(idx, itm){
1191 duration || (duration = parseFloat( el.css( itm ) ));
1192 });
1193
1194 // if I have a duration then add the class
1195 if (duration != 0) {
1196 el.addClass('is-transitioning');
1197 el[0].offsetWidth; // check offsetWidth to force the style rendering
1198 }
1199 });
1200};
1201
1202var selectors$5 = {
1203 passwordModal: '.password-modal',
1204 modalOpenButton: '.password__open-modal',
1205 modalCloseButton: '.password-modal__close',
1206 modalInput: '.password-modal__input'
1207};
1208var classes$5 = {
1209 passwordModalOpen: 'password-modal--open',
1210 disableScroll: 'disable-scroll'
1211};
1212
1213templates.register('Password Template', 'password-template', {
1214 onLoad: function(container) {
1215 var $container = (this.$container = $(container));
1216
1217 $(selectors$5.modalOpenButton, $container).on(
1218 'click',
1219 this.showPasswordModal.bind(this)
1220 );
1221 $(selectors$5.modalCloseButton, $container).on(
1222 'click',
1223 this.hidePasswordModal.bind(this)
1224 );
1225
1226 if ($('.errors', $container).length) {
1227 this.showPasswordModal();
1228 }
1229 },
1230
1231 showPasswordModal: function() {
1232 var $container = this.$container;
1233 var $modal = $(selectors$5.passwordModal, $container);
1234
1235 a11y$1.trapFocus({
1236 $container: $modal,
1237 namespace: 'passwordModal',
1238 $elementToFocus: $(selectors$5.modalCloseButton, $container)
1239 });
1240
1241 $modal.prepareTransition().addClass(classes$5.passwordModalOpen);
1242
1243 $(selectors$5.modalOpenButton, $container).attr('aria-expanded', true);
1244 $(selectors$5.modalInput, $container).focus();
1245 $(document.documentElement).addClass(classes$5.disableScroll);
1246 $(document).on('keyup', this.closeOnEscape.bind(this));
1247 },
1248
1249 hidePasswordModal: function() {
1250 var $container = this.$container;
1251 var $modal = $(selectors$5.passwordModal, $container);
1252
1253 a11y$1.removeTrapFocus({ namespace: 'passwordModal' });
1254
1255 $modal.prepareTransition().removeClass(classes$5.passwordModalOpen);
1256
1257 $(document.documentElement).removeClass(classes$5.disableScroll);
1258 $(selectors$5.modalOpenButton, $container)
1259 .attr('aria-expanded', false)
1260 .focus();
1261
1262 $(document).off('keyup', this.closeOnEscape.bind(this));
1263 },
1264
1265 closeOnEscape: function(evt) {
1266 if (evt.keyCode === 27) {
1267 this.hidePasswordModal();
1268 }
1269 }
1270});
1271
1272function Sections() {
1273 this.$document = $(document);
1274 this.namespace = '.section-js-events';
1275
1276 document.addEventListener(
1277 'shopify:section:load',
1278 function(evt) {
1279 var id = evt.detail.sectionId;
1280 var container = evt.target.querySelector(
1281 '[data-section-id="' + id + '"]'
1282 );
1283 var type = container.getAttribute('data-section-type');
1284
1285 this.load(type, container);
1286 }.bind(this)
1287 );
1288}
1289
1290$.extend(Sections.prototype, {
1291 /**
1292 * Indexed list of all registered section types
1293 */
1294 registered: {},
1295
1296 /**
1297 * List of all section instances
1298 */
1299 instances: [],
1300
1301 /**
1302 * Indexed list of all registered global extensions
1303 */
1304 extensions: {
1305 '*': []
1306 },
1307
1308 /**
1309 * Registers a section type with properties. Adds a new section constructor to
1310 * the registered list of sections.
1311 *
1312 * @param {string} type
1313 * @param {object} properties
1314 */
1315 register: function(type, properties) {
1316 function Section(data) {
1317 this.type = type;
1318 Master$1.call(this, data);
1319 }
1320
1321 Section.constructor = this.registered[type];
1322 Section.prototype = Object.create(Master$1.prototype);
1323 $.extend(Section.prototype, properties);
1324
1325 this.registered[type] = Section;
1326 },
1327
1328 /**
1329 * Loads all or the specified section types
1330 */
1331 load: function(types, containers) {
1332 types = this._normalizeTypeParam(types);
1333 containers = this._normalizeContainersParam(containers);
1334
1335 types.forEach(
1336 function(type) {
1337 var Section = this.registered[type];
1338 var selection = containers;
1339
1340 if (typeof Section === 'undefined') {
1341 return;
1342 }
1343
1344 if (typeof selection === 'undefined') {
1345 selection = document.querySelectorAll(
1346 '[data-section-type="' + type + '"]'
1347 );
1348 }
1349
1350 // Convert selection NodeList into an array
1351 selection = Array.prototype.slice.call(selection);
1352
1353 selection.forEach(
1354 function(container) {
1355 if (this._instanceExists(container)) {
1356 return;
1357 }
1358
1359 var extensions = this.extensions['*'].concat(
1360 this.extensions[type] || []
1361 );
1362 var instance = new Section({
1363 container: container,
1364 extensions: extensions,
1365 id: container.getAttribute('data-section-id')
1366 });
1367
1368 instance.trigger('section_load');
1369
1370 this.instances.push(instance);
1371 }.bind(this)
1372 );
1373 }.bind(this)
1374 );
1375 },
1376
1377 /**
1378 * Extend single, multiple, or all sections with additional functionality.
1379 */
1380 extend: function(types, extension) {
1381 types = this._normalizeTypeParam(types);
1382
1383 types.forEach(
1384 function(type) {
1385 this.extensions[type] = this.extensions[type] || [];
1386 this.extensions[type].push(extension);
1387
1388 if (typeof this.registered[type] === 'undefined') {
1389 return;
1390 }
1391
1392 this.instances.forEach(function(instance) {
1393 if (instance.type !== type) {
1394 return;
1395 }
1396 instance.extend(extension);
1397 });
1398 }.bind(this)
1399 );
1400 },
1401
1402 /**
1403 * Checks if a particular section type has been loaded on the page.
1404 */
1405 isInstance: function(type) {
1406 return typeof find(this.instances, { type: type }) === 'object';
1407 },
1408
1409 /**
1410 * Returns all instances of a section type on the page.
1411 */
1412 getInstances: function(type) {
1413 return $.Deferred(
1414 function(defer) {
1415 var instances = filter(this.instances, { type: type });
1416
1417 if (instances.length === 0) {
1418 defer.reject();
1419 } else {
1420 defer.resolve(instances);
1421 }
1422 }.bind(this)
1423 );
1424 },
1425
1426 /**
1427 * Attaches an event handler to the document that is fired whenever any section
1428 * instance triggers an event of specified type. Automatically adds a namespace
1429 * for easy removal with `sections.off('event')`
1430 */
1431 on: function() {
1432 // Convert arguments object into an array
1433 var args = Array.prototype.slice.call(arguments);
1434
1435 // Apply the section namespace to any event handler created by this section
1436 args[0] = args[0].concat(this.namespace);
1437
1438 this.$document.on.apply(this.$document, args);
1439 },
1440
1441 /**
1442 * Removes an event handler attached using `sections.on()`.
1443 */
1444 off: function() {
1445 // Convert arguments object into an array
1446 var args = Array.prototype.slice.call(arguments);
1447
1448 // Apply the section namespace to any event handler created by this section
1449 args[0] = args[0].concat(this.namespace);
1450
1451 this.$document.off.apply(this.$document, arguments);
1452 },
1453
1454 /**
1455 * Triggers and event in every section instance
1456 */
1457 trigger: function() {
1458 var triggerArgs = arguments;
1459 this.instances.forEach(function(instance) {
1460 instance.trigger.apply(instance, triggerArgs);
1461 });
1462 },
1463
1464 _sectionTrigger: function() {
1465 this.$document.trigger.apply(this.$document, arguments);
1466 },
1467
1468 _normalizeTypeParam: function(types) {
1469 if (types === '*') {
1470 types = Object.keys(this.registered);
1471 } else if (typeof types === 'string') {
1472 types = [types];
1473 }
1474
1475 types = types.map(function(type) {
1476 return type.toLowerCase();
1477 });
1478
1479 return types;
1480 },
1481
1482 _normalizeContainersParam: function(containers) {
1483 if (!Array.isArray(containers) && typeof containers === 'object') {
1484 // If a single container object is specified not inside a function
1485 containers = [containers];
1486 }
1487 return containers;
1488 },
1489
1490 _instanceExists: function(container) {
1491 var instance = find(this.instances, {
1492 id: container.getAttribute('data-section-id')
1493 });
1494 return typeof instance !== 'undefined';
1495 }
1496});
1497
1498var sections = new Sections();
1499/**
1500 * Master section class that all sections inherit from
1501 * @constructor
1502 *
1503 */
1504function Master$1(data) {
1505 this.container = data.container;
1506 this.$container = $(this.container);
1507 this.id = data.id;
1508 this.namespace = '.' + data.id;
1509 this.extensions = data.extensions || [];
1510 this.$eventBinder = this.$container;
1511
1512 _applyExtensions.call(this);
1513 _applyEditorHandlers.call(this);
1514 _applyDefaultHandlers.call(this);
1515}
1516
1517Master$1.prototype = {
1518 /* eslint-disable no-empty-function */
1519 onLoad: function() {},
1520 onUnload: function() {},
1521 onSelect: function() {},
1522 onDeselect: function() {},
1523 onBlockSelect: function() {},
1524 onBlockDeselect: function() {},
1525
1526 /* eslint-enable no-empty-function */
1527
1528 /**
1529 * Attaches an event handler to an instance of a section. Only listens to
1530 * events triggered by that section instance.
1531 */
1532 on: function() {
1533 // Convert arguments object into an array
1534 var args = Array.prototype.slice.call(arguments);
1535
1536 // Apply the section namespace to any event handler created by this section
1537 args[0] = args[0].concat(this.namespace);
1538
1539 this.$eventBinder.on.apply(this.$eventBinder, args);
1540 this.$eventBinder = this.$container;
1541 },
1542
1543 /**
1544 * Attaches an event handler to an instance of a section that is removed after
1545 * being called once. Only listens to events triggered by that section instance.
1546 */
1547 one: function() {
1548 // Convert arguments object into an array
1549 var args = Array.prototype.slice.call(arguments);
1550
1551 // Apply the section namespace to any event handler created by this section
1552 args[0] = args[0].concat(this.namespace);
1553
1554 this.$eventBinder.one.apply(this.$eventBinder, args);
1555 this.$eventBinder = this.$container;
1556 },
1557
1558 /**
1559 * Removes an event handler that was attached using the `this.on()` method
1560 */
1561 off: function() {
1562 // Convert arguments object into an array
1563 var args = Array.prototype.slice.call(arguments);
1564
1565 // Apply the section namespace to any event handler created by this section
1566 args[0] = args[0] || '';
1567 args[0] = args[0].concat(this.namespace);
1568
1569 this.$eventBinder.off.apply(this.$eventBinder, arguments);
1570 this.$eventBinder = this.$container;
1571 },
1572
1573 /*
1574 * Triggers an event on both this section instance and the sections object so
1575 * so that any event handlers attached using `sections.on()` will be also
1576 * triggered.
1577 */
1578 trigger: function() {
1579 // Convert arguments object into an array
1580 var args = Array.prototype.slice.call(arguments);
1581
1582 // Check what the second argument is. If there is already an array keep it.
1583 args[1] = args[1] || [];
1584
1585 // Add the section instance as the first item in the array. This will force
1586 // it to be the first param in the .on() callback
1587 args[1].splice(0, 0, this);
1588
1589 this.$eventBinder.trigger.apply(this.$eventBinder, args);
1590 this.$eventBinder = this.$container;
1591 },
1592
1593 /**
1594 * Extends this section instance with additional functionality.
1595 */
1596 extend: function(extension) {
1597 var init = extension.init;
1598 this.extensions.push(extension);
1599
1600 $.extend(this, omit(extension, 'init'));
1601
1602 if ($.isFunction(init)) {
1603 init.apply(this);
1604 }
1605 }
1606};
1607
1608/**
1609 * Shortcut methods that are automatically namespaced for easy removal, e.g.
1610 * $(document).on('event' + this.namespace);
1611 */
1612Master$1.prototype.document = function() {
1613 var $document = $(document);
1614 var self = this;
1615 return {
1616 on: function() {
1617 self.$eventBinder = $document;
1618 self.on.apply(self, arguments);
1619 },
1620 off: function() {
1621 self.$eventBinder = $document;
1622 self.off.apply(self, arguments);
1623 },
1624 trigger: function() {
1625 self.$eventBinder = $document;
1626 self.trigger.apply(self, arguments);
1627 }
1628 };
1629};
1630
1631/**
1632 * Shortcut methods that are automatically namespaced for easy removal, e.g.
1633 * $(window).on('event' + this.namespace);
1634 */
1635Master$1.prototype.window = function() {
1636 var $window = $(window);
1637 var self = this;
1638 return {
1639 on: function() {
1640 self.$eventBinder = $window;
1641 self.on.apply(self, arguments);
1642 },
1643 off: function() {
1644 self.$eventBinder = $window;
1645 self.off.apply(self, arguments);
1646 },
1647 trigger: function() {
1648 self.$eventBinder = $window;
1649 self.trigger.apply(self, arguments);
1650 }
1651 };
1652};
1653
1654function _applyExtensions() {
1655 this.extensions.forEach(
1656 function(extension) {
1657 this.extend(extension);
1658 }.bind(this)
1659 );
1660}
1661
1662function _applyEditorHandlers() {
1663 $(document)
1664 .on('shopify:section:unload' + this.namespace, _onSectionUnload.bind(this))
1665 .on('shopify:section:select' + this.namespace, _onSelect.bind(this))
1666 .on('shopify:section:deselect' + this.namespace, _onDeselect.bind(this))
1667 .on('shopify:block:select' + this.namespace, _onBlockSelect.bind(this))
1668 .on('shopify:block:deselect' + this.namespace, _onBlockDeselect.bind(this));
1669}
1670
1671function _applyDefaultHandlers() {
1672 this.on('section_load', this.onLoad.bind(this));
1673 this.on('section_unload', this.onUnload.bind(this));
1674 this.on('section_select', this.onSelect.bind(this));
1675 this.on('section_deselect', this.onDeselect.bind(this));
1676 this.on('block_select', this.onBlockSelect.bind(this));
1677 this.on('block_deselect', this.onBlockDeselect.bind(this));
1678}
1679
1680function _onSectionUnload(event) {
1681 if (this.id !== event.detail.sectionId) return;
1682
1683 event.type = 'section_unload';
1684 this.trigger(event);
1685
1686 this.off(this.namespace);
1687 sections.off(this.namespace);
1688 $(document).off(this.namespace);
1689 $(window).off(this.namespace);
1690
1691 remove(sections.instances, { id: this.id });
1692}
1693
1694function _onSelect(event) {
1695 if (this.id !== event.detail.sectionId) return;
1696
1697 event.type = 'section_select';
1698 this.trigger(event);
1699}
1700
1701function _onDeselect(event) {
1702 if (this.id !== event.detail.sectionId) return;
1703
1704 event.type = 'section_deselect';
1705 this.trigger(event);
1706}
1707
1708function _onBlockSelect(event) {
1709 if (this.id !== event.detail.sectionId) return;
1710
1711 event.type = 'block_select';
1712 this.trigger(event);
1713}
1714
1715function _onBlockDeselect(event) {
1716 if (this.id !== event.detail.sectionId) return;
1717
1718 event.type = 'block_deselect';
1719 this.trigger(event);
1720}
1721
1722/*
1723
1724Card Manager Extension
1725--------------------------------------------------------------------------------
1726Manages the drawer functionilty of the cart drawer section
1727
1728
1729Events
1730------------
1731
1732Name: cards_load_start
1733Description: Fired before cards start to load
1734Payload: none
1735
1736Name: card_loaded
1737Description: Fired each time a card is loaded onto the page
1738Payload: { object } Card DOM element
1739
1740Name: cards_load_done
1741Description: Fired when all cards have loaded
1742Payload: none
1743
1744*/
1745
1746var classes$6 = {
1747 cardListSubLoaded: 'card-list__sub-actions--loaded',
1748 cardReveal: 'card--reveal',
1749 searchTemplate: 'template-search'
1750};
1751
1752var selectors$6 = {
1753 cardList: '.card-list',
1754 cardListColumn: '.card-list__column',
1755 cardListSub: '.card-list__sub-actions',
1756 card: '.card',
1757 cardWrapper: '.card__wrapper',
1758 cardInfo: '.card__info'
1759};
1760
1761var cardManager = {
1762 init: function() {
1763 this.$cardList = $(selectors$6.cardList, this.$container);
1764 this.$emptyColumn = $(selectors$6.cardListColumn, this.$container)
1765 .clone()
1766 .empty();
1767 this.$cards = $(selectors$6.card, this.$container);
1768
1769 this.desktopColumnCount = this.$cardList.data('desktop-columns') || 2;
1770 this.mobileColumnCount = this.$cardList.data('mobile-columns') || 1;
1771 this.columnCount = utils.isMobile()
1772 ? this.mobileColumnCount
1773 : this.desktopColumnCount;
1774 this.gridStyle = this.$cardList.data('grid-style') || 'collage';
1775 this.windowWidth = $(window).outerWidth();
1776 this.cardLoadPromises = [];
1777
1778 this.window().on('resize', this._onResize.bind(this));
1779
1780 this.on('keydown', selectors$6.cardWrapper, this._onCardTabDown.bind(this));
1781 this.on('keyup', this._onCardTabUp.bind(this));
1782
1783 utils.promiseStylesheet().then(
1784 function() {
1785 this.cardsLoaded = this._loadCards();
1786
1787 $('html').removeClass('site-footer--hidden');
1788
1789 $(selectors$6.cardListSub, this.$container)
1790 .prepareTransition()
1791 .addClass(classes$6.cardListSubLoaded);
1792 }.bind(this)
1793 );
1794 },
1795
1796 _onResize: function() {
1797 var columnCount = utils.isMobile()
1798 ? this.mobileColumnCount
1799 : this.desktopColumnCount;
1800
1801 // Only replace columns if the number of columns changes.
1802 if (this.columnCount !== columnCount) {
1803 this.columnCount = columnCount;
1804 this.$columns = this._replaceColumns();
1805 }
1806
1807 $.each(
1808 this.$cards,
1809 function(index) {
1810 if (utils.isMobile()) {
1811 this.$columns.find(selectors$6.cardWrapper).removeAttr('style');
1812 } else if (this._isAlignedRow(index + 1)) {
1813 var rowNumber =
1814 (index - (index % this.desktopColumnCount)) /
1815 this.desktopColumnCount +
1816 1;
1817 this._matchRowHeights(this.$columns, rowNumber);
1818 }
1819 }.bind(this)
1820 );
1821 },
1822
1823 _onCardTabDown: function(evt) {
1824 if (evt.keyCode !== 9) return; // Only continue if tab key is pressed
1825
1826 var index = $(evt.target).data('tabindex');
1827
1828 this.tabFromList = true;
1829
1830 if (evt.shiftKey) {
1831 if (index > 1) {
1832 evt.preventDefault();
1833 $('[data-tabindex=' + (index - 1) + ']').focus();
1834 }
1835 } else {
1836 if (index === this.$cards.length) {
1837 $(selectors$6.cardWrapper, this.$container)
1838 .last()
1839 .focus();
1840 } else {
1841 evt.preventDefault();
1842 $('[data-tabindex=' + (index + 1) + ']').focus();
1843 }
1844 }
1845 },
1846
1847 _onCardTabUp: function(evt) {
1848 if (evt.keyCode !== 9) return;
1849
1850 var tabFromList = this.tabFromList || false;
1851 this.tabFromList = false;
1852
1853 if (!evt.shiftKey || tabFromList) return;
1854
1855 if (
1856 $(selectors$6.cardWrapper, this.$container)
1857 .last()
1858 .is(evt.target)
1859 ) {
1860 $('[data-tabindex=' + this.$cards.length + ']').focus();
1861 }
1862 },
1863
1864 _loadCards: function() {
1865 this.trigger('cards_load_start');
1866
1867 this.$columns = this._addColumns();
1868
1869 return this._promiseAllCardsReveal().always(
1870 function() {
1871 this.trigger('cards_load_done');
1872 }.bind(this)
1873 );
1874 },
1875
1876 _addColumns: function() {
1877 var $columns;
1878 for (var i = 1; i < this.columnCount; i++) {
1879 this.$cardList.append(this.$emptyColumn.clone());
1880 }
1881
1882 $columns = $(selectors$6.cardListColumn, this.$container);
1883 $columns.heights = fill(Array(this.columnCount), 0);
1884 $columns.lengths = fill(Array(this.columnCount), 0);
1885
1886 return $columns;
1887 },
1888
1889 _replaceColumns: function() {
1890 var $columns = this.$emptyColumn.clone();
1891
1892 $columns.heights = fill(Array(this.columnCount), 0);
1893 $columns.lengths = fill(Array(this.columnCount), 0);
1894
1895 for (var i = 1; i < this.columnCount; i++) {
1896 $columns.push(this.$emptyColumn.clone()[0]);
1897 }
1898
1899 $.each(
1900 this.$cards,
1901 function(index, card) {
1902 this._positionCard($(card), index, $columns);
1903 }.bind(this)
1904 );
1905
1906 this.$cardList.html($columns);
1907
1908 return $columns;
1909 },
1910
1911 _promiseAllCardsReveal: function() {
1912 var series = $.Deferred().resolve();
1913
1914 this.$cards.each(
1915 function(index, card) {
1916 // Position the card in one of the available columns
1917 this._positionCard($(card), index, this.$columns);
1918
1919 // Check that we have an aligned row and isMobile is false
1920 if (!utils.isMobile() && this._isAlignedRow(index + 1)) {
1921 var rowNumber = this.$columns.lengths[0];
1922 this._matchRowHeights(this.$columns, rowNumber);
1923 }
1924
1925 // We want to check immediately if the card should be revealed, not after
1926 // a previous `.then()` in our series has been fulfilled.
1927 var promiseRevealReady = this._promiseRevealReady(card);
1928
1929 // Add the following steps to our series of promises for each card:
1930 // 1. Promise the card is ready to be revealed
1931 // 2. Reveal the card
1932 // 3. Delay 80ms
1933 series = series
1934 .then(function() {
1935 return promiseRevealReady;
1936 })
1937 .then(this._revealCard.bind(this, card))
1938 .then(function() {
1939 return $.Deferred(function(defer) {
1940 setTimeout(defer.resolve, 80);
1941 });
1942 });
1943 }.bind(this)
1944 );
1945 return series;
1946 },
1947
1948 _revealCard: function(card) {
1949 var $card = $(card);
1950
1951 this.trigger('card_loaded', [$card[0]]);
1952
1953 requestAnimationFrame($card.addClass.bind($card, classes$6.cardReveal));
1954 },
1955
1956 // A promise that will be fulfilled when a card is ready to be revealed
1957 _promiseRevealReady: function(card) {
1958 return $.Deferred(function(defer) {
1959 var $card = $(card);
1960
1961 if (!($('html').hasClass('is-ios') && Shopify.designMode)) {
1962 // If we already triggered the animation, then resolve the promise to show
1963 // the card. Wrap the position check in requestAnimationFrame to make sure
1964 // that the browser is done positioning the element before it reads its
1965 // position.
1966 requestAnimationFrame(function() {
1967 if (utils.isInOrAboveViewport(card)) {
1968 return defer.resolve();
1969 }
1970 });
1971
1972 // If we haven't triggered the animation, then wait for the animation
1973 // event for this card and then resolve the promise to show the card.
1974 $card.on('animate_element', onAnimate);
1975
1976 function onAnimate(evt, element) {
1977 if (element !== card) return;
1978
1979 $card.off('animate_element', onAnimate);
1980 defer.resolve();
1981 }
1982 } else {
1983 $(card).addClass(classes$6.cardReveal);
1984 defer.resolve();
1985 }
1986 });
1987 },
1988
1989 _positionCard: function($card, index, $columns) {
1990 var columnIndex = 0;
1991 var cardHeight = $card.outerHeight(true);
1992
1993 if (this.gridStyle === 'collage') {
1994 columnIndex = $columns.heights.indexOf(
1995 Math.min.apply(Math, $columns.heights)
1996 ); // Default to shortest column
1997 var threshold = 150;
1998 for (var i = 0; i < $columns.heights.length; i++) {
1999 if ($columns.heights[columnIndex] > $columns.heights[i] - threshold) {
2000 columnIndex = i;
2001 break;
2002 }
2003 }
2004 } else {
2005 columnIndex = $columns.lengths.indexOf(
2006 Math.min.apply(Math, $columns.lengths)
2007 );
2008 }
2009
2010 $columns.eq(columnIndex).append($card);
2011
2012 // Restore the height if it is 0 (needed for matching heights in the
2013 // 'aligned' grid)
2014 if ($card.outerHeight() === 0 && !utils.isMobile()) {
2015 $card.find(selectors$6.cardWrapper).outerHeight(cardHeight);
2016 }
2017
2018 if (this.gridStyle === 'collage') {
2019 $columns.heights[columnIndex] += cardHeight;
2020 }
2021 $columns.lengths[columnIndex]++;
2022
2023 $card.find('a').attr('data-tabindex', index + 1);
2024 },
2025
2026 _matchRowHeights: function($columns, rowNumber) {
2027 var maxHeight = 0;
2028 var currentCardHeight = 0;
2029 var rowSelector = '';
2030 var isArticle = false;
2031
2032 // Construct the selector for the cards in the current row
2033 for (var i = 0; i < $columns.length; i++) {
2034 rowSelector +=
2035 "[data-tabindex='" + ((rowNumber - 1) * $columns.length + i + 1) + "']";
2036 if (i < $columns.length - 1) {
2037 rowSelector += ', ';
2038 }
2039 }
2040 var $row = $columns.find(rowSelector).parent();
2041
2042 for (var j = 0; j < $row.length; j++) {
2043 isArticle = $($row[j])
2044 .find(selectors$6.cardWrapper)
2045 .is('.card--article');
2046
2047 currentCardHeight = $($row[j])
2048 .find(selectors$6.cardWrapper)
2049 .outerWidth();
2050 if ($(document.body).hasClass(classes$6.searchTemplate) && isArticle) {
2051 currentCardHeight = $($row[j])
2052 .find(selectors$6.cardWrapper)
2053 .outerHeight();
2054 } else {
2055 currentCardHeight =
2056 $($row[j])
2057 .find(selectors$6.cardWrapper)
2058 .outerWidth() +
2059 $($row[j])
2060 .find(selectors$6.cardInfo)
2061 .outerHeight();
2062 }
2063
2064 if (currentCardHeight > maxHeight) {
2065 maxHeight = currentCardHeight;
2066 }
2067 }
2068
2069 $row.find(selectors$6.cardWrapper).outerHeight(maxHeight);
2070 },
2071
2072 _isAlignedRow: function(index) {
2073 // Check if the gridStyle is 'grid', and if we are at the end of a row or
2074 // at the last card
2075 return (
2076 this.gridStyle === 'grid' &&
2077 (index % this.desktopColumnCount === 0 || index === this.$cards.length)
2078 );
2079 }
2080};
2081
2082/*
2083
2084Blog Template Section
2085--------------------------------------------------------------------------------
2086Creates a staggered card grid for the blog template.
2087
2088*/
2089
2090// Extensions
2091// Libs
2092sections.register('blog-template', {
2093 onLoad: function() {
2094 this.extend(cardManager);
2095 }
2096});
2097
2098var cart = {
2099 isUpdating: false,
2100
2101 getCart: function() {
2102 return $.getJSON('/cart.js');
2103 },
2104
2105 on: function(event, cb) {
2106 $(this).on(event, cb);
2107 },
2108
2109 trigger: function(event, data) {
2110 $(this).triggerHandler(event, data);
2111 },
2112
2113 updateNote: function(note) {
2114 return this._promiseChange({
2115 url: '/cart/update.js',
2116 dataType: 'json',
2117 data: {
2118 note: note || ''
2119 }
2120 });
2121 },
2122
2123 addItem: function(id, quantity) {
2124 return this._promiseChange({
2125 url: '/cart/add.js',
2126 dataType: 'json',
2127 data: {
2128 id: id,
2129 quantity: typeof quantity === 'undefined' ? 1 : quantity
2130 }
2131 });
2132 },
2133
2134 addItemFromForm: function(data) {
2135 return this._promiseChange({
2136 url: '/cart/add.js',
2137 dataType: 'json',
2138 processData: false,
2139 contentType: false,
2140 type: 'POST',
2141 data: data
2142 });
2143 },
2144
2145 removeItem: function(id) {
2146 return this._promiseChange({
2147 url: '/cart/change.js',
2148 dataType: 'json',
2149 data: {
2150 id: id,
2151 quantity: 0
2152 }
2153 });
2154 },
2155
2156 changeItem: function(id, quantity) {
2157 return this._promiseChange({
2158 url: '/cart/change.js',
2159 dataType: 'json',
2160 data: {
2161 id: id,
2162 quantity: quantity
2163 }
2164 });
2165 },
2166
2167 saveLocalState: function(state) {
2168 if (utils.isLocalStorageSupported()) {
2169 localStorage.shopify_cart_state = JSON.stringify(state); // eslint-disable-line camelcase
2170 }
2171
2172 return state;
2173 },
2174
2175 getLocalState: function() {
2176 // eslint-disable-line consistent-return
2177 if (utils.isLocalStorageSupported()) {
2178 return JSON.parse(localStorage.shopify_cart_state || '');
2179 }
2180 },
2181
2182 _promiseChange: function(parameters) {
2183 var promiseRequest = $.ajax(parameters);
2184
2185 this.isUpdating = true;
2186
2187 // If offline, provide a rejected promise so that an error is thrown.
2188 if (navigator && !theme.isOnline) {
2189 promiseRequest = $.Deferred().reject();
2190 }
2191
2192 return (
2193 promiseRequest
2194 // Some cart API requests don't return the cart object. If there is no
2195 // cart object then get one before proceeding.
2196 .then(
2197 function(state) {
2198 if (typeof state.token === 'undefined') {
2199 return this.getCart();
2200 } else {
2201 return state;
2202 }
2203 }.bind(this)
2204 )
2205 .then(this.saveLocalState)
2206 .then(this._triggerChangeEvent.bind(this))
2207 .catch(this._triggerErrorEvent.bind(this))
2208 .always(
2209 function() {
2210 this.isUpdating = false;
2211 }.bind(this)
2212 )
2213 );
2214 },
2215
2216 _triggerChangeEvent: function(state) {
2217 this.trigger('change', state);
2218 return state;
2219 },
2220
2221 _triggerErrorEvent: function(error) {
2222 this.trigger('error', error);
2223
2224 // We want to throw an error event, but also let any future 'catch' down
2225 // the promise chain to catch this error, so we re-throw the error.
2226 throw error;
2227 }
2228};
2229
2230/**
2231 * Currency Helpers
2232 * -----------------------------------------------------------------------------
2233 * A collection of useful functions that help with currency formatting
2234 *
2235 * Current contents
2236 * - formatMoney - Takes an amount in cents and returns it as a formatted dollar value.
2237 *
2238 * Alternatives
2239 * - Accounting.js - http://openexchangerates.github.io/accounting.js/
2240 *
2241 */
2242
2243var moneyFormat = '${{amount}}'; // eslint-disable-line camelcase
2244
2245var currency = {
2246 formatMoney: function(cents, format) {
2247 if (typeof cents === 'string') {
2248 cents = cents.replace('.', '');
2249 }
2250 var value = '';
2251 var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/;
2252 var formatString = format || moneyFormat;
2253
2254 function formatWithDelimiters(number, precision, thousands, decimal) {
2255 precision = defaultTo(precision, 2);
2256 thousands = defaultTo(thousands, ',');
2257 decimal = defaultTo(decimal, '.');
2258
2259 if (isNaN(number) || number === null) {
2260 return 0;
2261 }
2262
2263 number = (number / 100.0).toFixed(precision);
2264
2265 var parts = number.split('.');
2266 var dollarsAmount = parts[0].replace(
2267 /(\d)(?=(\d\d\d)+(?!\d))/g,
2268 '$1' + thousands
2269 );
2270 var centsAmount = parts[1] ? decimal + parts[1] : '';
2271
2272 return dollarsAmount + centsAmount;
2273 }
2274
2275 switch (formatString.match(placeholderRegex)[1]) {
2276 case 'amount':
2277 value = formatWithDelimiters(cents, 2);
2278 break;
2279 case 'amount_no_decimals':
2280 value = formatWithDelimiters(cents, 0);
2281 break;
2282 case 'amount_with_comma_separator':
2283 value = formatWithDelimiters(cents, 2, '.', ',');
2284 break;
2285 case 'amount_no_decimals_with_comma_separator':
2286 value = formatWithDelimiters(cents, 0, '.', ',');
2287 break;
2288 case 'amount_no_decimals_with_space_separator':
2289 value = formatWithDelimiters(cents, 0, ' ');
2290 break;
2291 case 'amount_with_apostrophe_separator':
2292 value = formatWithDelimiters(cents, 2, "'");
2293 break;
2294 }
2295
2296 return formatString.replace(placeholderRegex, value);
2297 }
2298};
2299
2300/**
2301 * Image Helper Functions
2302 * -----------------------------------------------------------------------------
2303 * A collection of functions that help with basic image operations.
2304 *
2305 */
2306
2307var images = {
2308 /**
2309 * Preloads an image in memory and uses the browsers cache to store it until needed.
2310 *
2311 * @param {Array} images - A list of image urls
2312 * @param {String} size - A shopify image size attribute
2313 */
2314
2315 preload: function(images, size) {
2316 if (typeof images === 'string') {
2317 images = [images];
2318 }
2319
2320 for (var i = 0; i < images.length; i++) {
2321 var image = images[i];
2322
2323 this.loadImage(this.getSizedImageUrl(image, size));
2324 }
2325 },
2326
2327 /**
2328 * Loads and caches an image in the browsers cache.
2329 * @param {string} path - An image url
2330 */
2331 loadImage: function(path) {
2332 new Image().src = path;
2333 },
2334
2335 /**
2336 * Swaps the src of an image for another OR returns the imageURL to the callback function
2337 * @param image
2338 * @param element
2339 * @param callback
2340 */
2341 switchImage: function(image, element, callback) {
2342 var size = this.imageSize(element.src);
2343 var imageUrl = this.getSizedImageUrl(image.src, size);
2344
2345 if (callback) {
2346 callback(imageUrl, image, element); // eslint-disable-line callback-return
2347 } else {
2348 element.src = imageUrl;
2349 }
2350 },
2351
2352 /**
2353 * +++ Useful
2354 * Find the Shopify image attribute size
2355 *
2356 * @param {string} src
2357 * @returns {null}
2358 */
2359 imageSize: function(src) {
2360 src = src || '';
2361
2362 var match = src.match(
2363 /.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\\.@]/
2364 );
2365
2366 if (match !== null) {
2367 return match[1];
2368 }
2369
2370 return null;
2371 },
2372
2373 /**
2374 * +++ Useful
2375 * Adds a Shopify size attribute to a URL
2376 *
2377 * @param src
2378 * @param size
2379 * @returns {*}
2380 */
2381 getSizedImageUrl: function(src, size) {
2382 if (size === null) {
2383 return src;
2384 }
2385
2386 if (size === 'master') {
2387 return this.removeProtocol(src);
2388 }
2389
2390 var match = src.match(
2391 /\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i
2392 );
2393
2394 if (match !== null) {
2395 var prefix = src.split(match[0]);
2396 var suffix = match[0];
2397
2398 return this.removeProtocol(prefix[0] + '_' + size + suffix);
2399 }
2400
2401 return null;
2402 },
2403
2404 removeProtocol: function(path) {
2405 return path.replace(/http(s)?:/, '');
2406 }
2407};
2408
2409/*
2410
2411Ajax Cart Extension
2412--------------------------------------------------------------------------------
2413Manages the AJAX powered cart functionality
2414
2415
2416Events
2417------------
2418
2419Name: cart_update_start
2420Description: Fired before the cart DOM updates
2421Payload: { object } Cart state object
2422
2423Name: cart_update_done
2424Description: Fired after the cart DOM updates
2425Payload: { object } Cart state object
2426
2427Name: cart_error
2428Description: Fired when there is an with the last cart request
2429Payload: { object } Error object
2430
2431Name: cart_item_quantity_error
2432Description: Fired when there is problem with the requested item quanitity
2433Payload: { object } Item with valid quantity
2434
2435Name: cart_storage_state_change
2436Description: Fired when the cart state changes in another active tab
2437Payload: { object } Cart state object
2438
2439Name: cart_item_quantity_change
2440Description: Fired when the user selects a new quantity in the item input
2441Payload: { string } Item ID
2442Payload: { number } Quantity
2443
2444Name: cart_item_remove
2445Description: Fired when the user selects to remove an item
2446Payload: { string } Item ID
2447
2448Name: cart_note_change
2449Description: Fired when the user changes the value in the note textarea
2450Payload: { string } Note value
2451
2452Name: cart_submit
2453Description: Fired when the user submits the cart form and proceeds to the checkout
2454Payload: none
2455
2456*/
2457
2458var data = {
2459 itemId: 'data-cart-item-id'
2460};
2461
2462var selectors$8 = {
2463 ajaxCart: '.cart-drawer',
2464 itemList: '[data-cart-item-list]',
2465 item: '[data-cart-item]',
2466 itemId: '[data-cart-item-id]',
2467 itemHref: '[data-cart-item-href]',
2468 itemImage: '[data-cart-item-image]',
2469 itemBackgroundImage: '[data-cart-item-background-image]',
2470 itemTitle: '[data-cart-item-title]',
2471 itemVariantTitle: '[data-cart-item-variant-title]',
2472 itemPropertyList: '[data-cart-item-property-list]',
2473 itemProperty: '[data-cart-item-property]',
2474 itemDiscountList: '[data-cart-item-discount-list]',
2475 itemDiscount: '[data-cart-item-discount]',
2476 itemDiscountTitle: '[data-cart-item-discount-title]',
2477 itemDiscountAmount: '[data-cart-item-discount-amount]',
2478 itemLabelQuantity: '[data-cart-item-label-quantity]',
2479 itemInputQuantity: '[data-cart-item-input-quantity]',
2480 itemDelete: '[data-cart-item-delete]',
2481 itemPriceContainer: '[data-cart-item-price-container]',
2482 itemLinePriceContainer: '[data-cart-item-line-price-container]',
2483 itemMessage: '[data-item-message]',
2484 cartDiscountContainer: '[data-cart-discount-container]',
2485 cartDiscount: '[data-cart-discount]',
2486 cartDiscountTitle: '[data-cart-discount-title]',
2487 cartDiscountAmount: '[data-cart-discount-amount]',
2488 cartNoteContainer: '[data-cart-note-container]',
2489 cartNoteInput: '[data-cart-note]',
2490 cartMessage: '[data-cart-message]',
2491 cartSubtotal: '[data-cart-subtotal]',
2492 cartSubmit: '[data-cart-submit]'
2493};
2494
2495var classes$8 = {
2496 cartTemplate: 'ajax-cart__template',
2497 cartItemRemove: 'ajax-cart__item--remove',
2498 cartError: 'ajax-cart--error',
2499 visuallyHidden: 'visually-hidden',
2500 cartDiscountActive: 'ajax-cart__discount-container--active',
2501 btnLoaderActive: 'btn--loader-active'
2502};
2503
2504var ajaxCart = {
2505 init: function() {
2506 this.$ajaxCart = $(selectors$8.ajaxCart, this.$container);
2507 this.$itemTemplate = $(selectors$8.item, this.$container)
2508 .first()
2509 .clone();
2510 this.$propertyTemplate = $(selectors$8.itemProperty, this.$container)
2511 .first()
2512 .clone();
2513 this.$discountTemplate = $(selectors$8.itemDiscount, this.$container)
2514 .first()
2515 .clone();
2516 this.$cartDiscountTemplate = $(selectors$8.cartDiscount, this.$container)
2517 .first()
2518 .clone();
2519
2520 cart.on('quantity', this._onQuantityError.bind(this));
2521 cart.on(
2522 'change',
2523 function(event, state) {
2524 this.update(state);
2525 }.bind(this)
2526 );
2527
2528 this.on(
2529 'input',
2530 selectors$8.itemInputQuantity,
2531 debounce(this._onItemQuantityChange.bind(this), 500)
2532 );
2533 this.on(
2534 'blur',
2535 selectors$8.itemInputQuantity,
2536 this._onItemQuantityEmptyBlur.bind(this)
2537 );
2538 this.on('focus', selectors$8.itemInputQuantity, this._highlightText);
2539 this.on('click', selectors$8.itemDelete, this._onItemDelete.bind(this));
2540 this.on('change', selectors$8.cartNoteInput, this._onNoteChange.bind(this));
2541 this.on('submit', this._onSubmit.bind(this));
2542
2543 this.window().on('storage', this._onStorageStateChange.bind(this));
2544 },
2545
2546 refresh: function() {
2547 cart.getCart().then(this.update.bind(this));
2548 },
2549
2550 update: function(state) {
2551 this.trigger('cart_update_start', [state]);
2552
2553 var $cart = this._createCart(state);
2554 morphdom(this.$container[0], $cart[0]);
2555
2556 this.trigger('cart_update_done', [state]);
2557 },
2558
2559 _onError: function(error) {
2560 this.trigger('cart_error', [error]);
2561
2562 this.$container.addClass(classes$8.cartError);
2563
2564 $(selectors$8.item, this.$container).removeClass(classes$8.cartItemRemove);
2565 $(selectors$8.cartMessage, this.$container).text(theme.strings.cartError);
2566 },
2567
2568 _onQuantityError: function(event, item) {
2569 this.trigger('cart_item_quantity_error', [item]);
2570
2571 this.$container.addClass(classes$8.cartError);
2572
2573 var quantityError = theme.strings.cartQuantityError
2574 .replace('[quantity]', item.quantity)
2575 .replace('[title]', item.title);
2576
2577 var $itemMessageElement = $(
2578 selectors$8.itemMessage,
2579 $('[' + data.itemId + '="' + item.key + '"]')
2580 );
2581
2582 $itemMessageElement.removeAttr('aria-hidden');
2583 $itemMessageElement.html(quantityError);
2584 },
2585
2586 _createCart: function(state) {
2587 var $container = this.$container.clone();
2588
2589 $container.removeClass(classes$8.cartError);
2590 $(selectors$8.cartMessage, $container).text('');
2591
2592 $(selectors$8.item, $container)
2593 .not(selectors$8.cartNoteContainer)
2594 .remove();
2595
2596 $(selectors$8.itemList, $container).prepend(this._createItemList(state));
2597
2598 $(selectors$8.cartNoteInput, $container).val(state.note);
2599
2600 $(selectors$8.cartDiscountContainer, $container).toggleClass(
2601 classes$8.cartDiscountActive,
2602 state.total_discount !== 0
2603 );
2604
2605 $(selectors$8.cartDiscountContainer, $container).html(
2606 this._createCartDiscountList(state)
2607 );
2608
2609 $(selectors$8.cartSubtotal, $container).html(
2610 currency.formatMoney(state.total_price, theme.moneyFormat)
2611 );
2612
2613 $(selectors$8.cartSubmit, $container).attr(
2614 'disabled',
2615 state.items.length === 0
2616 );
2617
2618 return $container;
2619 },
2620
2621 _createItemList: function(state) {
2622 return $.map(
2623 state.items,
2624 function(item) {
2625 var $item = this.$itemTemplate
2626 .clone()
2627 .removeClass(classes$8.cartTemplate);
2628 var propertyList = this._createPropertyList(item);
2629 var discountList = this._createDiscountList(item);
2630 var itemPrice = this._createItemPrice(item);
2631 var itemLinePrice = this._createItemTotalPrice(item);
2632
2633 $item
2634 .find(selectors$8.itemId)
2635 .addBack(selectors$8.itemId)
2636 .attr(data.itemId, item.key);
2637
2638 $(selectors$8.itemHref, $item).attr('href', item.url);
2639
2640 $(selectors$8.itemImage, $item)
2641 .attr(
2642 'src',
2643 item.image ? images.getSizedImageUrl(item.image, 'medium') : ''
2644 )
2645 .toggleClass('hide', typeof item.image !== 'string');
2646
2647 $(selectors$8.itemBackgroundImage, $item).css(
2648 'background-image',
2649 item.image
2650 ? 'url(' + images.getSizedImageUrl(item.image, 'medium') + ')'
2651 : 'none'
2652 );
2653
2654 $(selectors$8.itemTitle, $item).text(item.product_title);
2655
2656 $(selectors$8.itemVariantTitle, $item).text(item.variant_title);
2657
2658 $(selectors$8.itemPriceContainer, $item).html(itemPrice);
2659
2660 $(selectors$8.itemLinePriceContainer, $item).html(itemLinePrice);
2661
2662 $(selectors$8.itemLabelQuantity, $item).attr(
2663 'for',
2664 'quantity_' + item.key
2665 );
2666
2667 $(selectors$8.itemInputQuantity, $item)
2668 .attr('name', 'updates[' + item.key + ']')
2669 .attr('id', 'quantity_' + item.key)
2670 .val(item.quantity);
2671
2672 $(selectors$8.itemPropertyList, $item).html(propertyList);
2673
2674 $(selectors$8.itemDiscountList, $item).html(discountList);
2675
2676 return $item[0];
2677 }.bind(this)
2678 );
2679 },
2680
2681 _createItemPrice: function(item) {
2682 var itemPrice = '';
2683
2684 itemPrice += this._createItemProductPrice(
2685 item.original_price,
2686 item.final_price
2687 );
2688
2689 if (item.unit_price_measurement) {
2690 itemPrice += this._createItemUnitPrice(item);
2691 }
2692
2693 return itemPrice;
2694 },
2695
2696 _createItemTotalPrice: function(item) {
2697 var itemPrice = '';
2698
2699 itemPrice += this._createItemProductPrice(
2700 item.original_line_price,
2701 item.final_line_price
2702 );
2703
2704 return itemPrice;
2705 },
2706
2707 _createItemProductPrice: function(original_price, final_price) {
2708 var itemPrice = '';
2709
2710 if (original_price !== final_price) {
2711 itemPrice +=
2712 '<span class="visually-hidden">' +
2713 theme.strings.regularPrice +
2714 '</span>' +
2715 '<del class="cart-item__original-price">' +
2716 currency.formatMoney(original_price, theme.moneyFormat) +
2717 '</del>' +
2718 '<span class="visually-hidden">' +
2719 theme.strings.salePrice +
2720 '</span>' +
2721 '<span class="order-discount cart-item__price">' +
2722 currency.formatMoney(final_price, theme.moneyFormat) +
2723 '</span>';
2724 } else {
2725 itemPrice +=
2726 '<span class="cart-item__original-price cart-item__price">' +
2727 currency.formatMoney(original_price, theme.moneyFormat) +
2728 '</span>';
2729 }
2730
2731 return itemPrice;
2732 },
2733
2734 _createItemUnitPrice: function(item) {
2735 return (
2736 '<span class="product-price-unit">' +
2737 '<span class="visually-hidden">' +
2738 theme.strings.unitPrice +
2739 '</span>' +
2740 '<span>' +
2741 currency.formatMoney(item.unit_price, theme.moneyFormat) +
2742 '</span>' +
2743 '<span aria-hidden="true">/</span>' +
2744 '<span class="visually-hidden"> ' +
2745 theme.strings.unitPriceSeparator +
2746 ' </span>' +
2747 '<span>' +
2748 (item.unit_price_measurement.reference_value !== 1
2749 ? item.unit_price_measurement.reference_value
2750 : '') +
2751 item.unit_price_measurement.reference_unit +
2752 '</span>' +
2753 '</span>'
2754 );
2755 },
2756
2757 _createPropertyList: function(item) {
2758 return $.map(
2759 item.properties,
2760 function(value, key) {
2761 var $property = this.$propertyTemplate
2762 .clone()
2763 .removeClass(classes$8.cartTemplate);
2764
2765 // Line item properties prefixed with an underscore are not to be displayed
2766 if (key.charAt(0) === '_') return;
2767
2768 // Line item properties with no value are not to be displayed
2769 if (value === '') return;
2770
2771 if (value.indexOf('/uploads/') === -1) {
2772 $property.text(key + ': ' + value);
2773 } else {
2774 $property.html(
2775 key + ': <a href="' + value + '">' + value.split('/').pop() + '</a>'
2776 );
2777 }
2778
2779 return $property[0];
2780 }.bind(this)
2781 );
2782 },
2783
2784 _createDiscountList: function(item) {
2785 return $.map(
2786 item.line_level_discount_allocations,
2787 function(discount) {
2788 var $discount = this.$discountTemplate
2789 .clone()
2790 .removeClass(classes$8.cartTemplate);
2791 $discount
2792 .find(selectors$8.itemDiscountTitle)
2793 .text(discount.discount_application.title);
2794 $discount
2795 .find(selectors$8.itemDiscountAmount)
2796 .html(currency.formatMoney(discount.amount, theme.moneyFormat));
2797 return $discount[0];
2798 }.bind(this)
2799 );
2800 },
2801
2802 _createCartDiscountList: function(cart$$1) {
2803 return $.map(
2804 cart$$1.cart_level_discount_applications,
2805 function(discount) {
2806 var $discount = this.$cartDiscountTemplate
2807 .clone()
2808 .removeClass(classes$8.cartTemplate);
2809 $discount.find(selectors$8.cartDiscountTitle).text(discount.title);
2810 $discount
2811 .find(selectors$8.cartDiscountAmount)
2812 .html(
2813 currency.formatMoney(
2814 discount.total_allocated_amount,
2815 theme.moneyFormat
2816 )
2817 );
2818 return $discount[0];
2819 }.bind(this)
2820 );
2821 },
2822
2823 _onStorageStateChange: function(evt) {
2824 if (evt.key !== 'shopify_cart_state') return;
2825
2826 var state = cart.getLocalState();
2827
2828 this.trigger('cart_storage_state_change', [state]);
2829 this.update(cart.getLocalState());
2830 },
2831
2832 _onItemQuantityChange: function(evt) {
2833 var $input = $(evt.target);
2834 var id = $input.closest(selectors$8.item).attr(data.itemId);
2835 var quantity = $input.val();
2836
2837 // Don't update the cart when a input is empty. Also make sure an input
2838 // does not remain empty by checking blur event.
2839 if (quantity === '') return;
2840
2841 this.trigger('cart_item_quantity_change', [id, quantity]);
2842
2843 cart.changeItem(id, quantity).catch(this._onError.bind(this));
2844 },
2845
2846 _onItemQuantityEmptyBlur: function(evt) {
2847 var $input = $(evt.target);
2848 var value = $input.val();
2849
2850 if (value !== '') return;
2851
2852 this.refresh();
2853 },
2854
2855 _onItemDelete: function(evt) {
2856 evt.preventDefault();
2857
2858 var $deleteButton = $(evt.target);
2859 var $items = $(selectors$8.item, this.$container);
2860 var $item = $deleteButton.closest(selectors$8.item);
2861 var $note = $(selectors$8.cartNoteContainer, this.$container);
2862 var id = $item.attr(data.itemId);
2863
2864 this.trigger('cart_item_remove', [id]);
2865
2866 if ($items.length === 2 && $items.last().is($note)) {
2867 $note.addClass(classes$8.cartItemRemove);
2868
2869 utils
2870 .promiseTransitionEnd($(selectors$8.itemList, this.$container))
2871 .then(function() {
2872 $note.removeClass(classes$8.cartItemRemove);
2873 });
2874 }
2875
2876 $item.addClass(classes$8.cartItemRemove);
2877
2878 utils.promiseAnimationEnd($item).then(
2879 function() {
2880 cart.removeItem(id).catch(this._onError.bind(this));
2881 }.bind(this)
2882 );
2883 },
2884
2885 _onNoteChange: function(evt) {
2886 var value = $(evt.target).val();
2887
2888 this.trigger('cart_note_change', [value]);
2889
2890 cart.updateNote(value).catch(this._onError.bind(this));
2891 },
2892
2893 _onSubmit: function(evt) {
2894 if (cart.isUpdating) {
2895 evt.preventDefault();
2896 } else {
2897 $(selectors$8.cartSubmit, this.$container).addClass(
2898 classes$8.btnLoaderActive
2899 );
2900 this.trigger('cart_submit');
2901 }
2902 },
2903
2904 _highlightText: function(evt) {
2905 $(evt.target).select();
2906 }
2907};
2908
2909/*
2910
2911Cart Drawer Extension
2912--------------------------------------------------------------------------------
2913Manages the drawer functionality of the cart drawer section
2914
2915
2916Events
2917------------
2918
2919Name: drawer_open_start
2920Description: Fired before the cart drawer starts to open
2921Payload: none
2922
2923Name: drawer_open_transitioning
2924Description: Fired while the cart drawer is transitioning open
2925Payload: none
2926
2927Name: drawer_open_done
2928Description: Fired while the cart drawer is done transitioning open
2929Payload: none
2930
2931Name: drawer_close_start
2932Description: Fired before the cart drawer starts to close
2933Payload: none
2934
2935Name: drawer_close_transitioning
2936Description: Fired while the cart drawer is transitioning close
2937Payload: none
2938
2939Name: drawer_close_done
2940Description: Fired while the cart drawer is done transitioning close
2941Payload: none
2942
2943Name: drawer_dragging
2944Description: Fired while the cart drawer is being dragged closed by the user
2945Payload: { object } Coordinates
2946
2947*/
2948
2949var classes$9 = {
2950 drawerActive: 'drawer--active',
2951 disableScroll: 'disable-scroll'
2952};
2953
2954var selectors$9 = {
2955 drawerPush: '[data-drawer-push]',
2956 drawerCover: '.drawer-cover',
2957 mainContent: '.main-content',
2958 siteHeader: '.site-header'
2959};
2960
2961var drawer = {
2962 init: function() {
2963 // Used to keep track of which button toggled the drawer so we can return
2964 // focus to it when the drawer closes.
2965 this.$toggleButton = null;
2966 this.draggingAction = null;
2967 this.namespaceTouch = '.onTouchStart';
2968 this.namespaceOpen = '.onOpen';
2969
2970 // Drawer width might change when resizing the window. Make sure that we have
2971 // the right drawer width to translate the page with.
2972 this.document().on('resize', this._onWindowResize.bind(this));
2973 },
2974
2975 drawerOpen: function(evt) {
2976 this.trigger('drawer_open_start');
2977
2978 a11y$1.trapFocus({
2979 $container: this.$container,
2980 namespace: 'drawer'
2981 });
2982
2983 // If this function is used as an event handler, e.g. a click event on a
2984 // button to open the drawer, than make sure to set whatever element that
2985 // triggered the event to aria-expanded = true
2986 if (evt && evt.currentTarget) {
2987 this.$toggleButton = $(evt.target).attr('aria-expanded', true);
2988 }
2989
2990 utils.disableScrollBody();
2991
2992 $('body').addClass(classes$9.drawerActive);
2993 this.trigger('drawer_open_transitioning');
2994
2995 utils.promiseTransitionEnd(this.$container).then(
2996 function() {
2997 utils.enableTabbingOfChildren(this.$container);
2998 this.$container.attr('aria-hidden', false);
2999
3000 this.document().on(
3001 'click touchend' + this.namespaceOpen,
3002 selectors$9.drawerCover,
3003 this.drawerClose.bind(this)
3004 );
3005 this.document().on(
3006 'keyup' + this.namespaceOpen,
3007 this._closeOnEscape.bind(this)
3008 );
3009 this.document().on(
3010 'touchstart' + this.namespaceOpen,
3011 this._onTouchStart.bind(this)
3012 );
3013
3014 this.trigger('drawer_open_done');
3015 }.bind(this)
3016 );
3017 },
3018
3019 drawerClose: function() {
3020 this.trigger('drawer_close_start');
3021
3022 a11y$1.removeTrapFocus({ namespace: 'drawer' });
3023
3024 // If we detected a toggle button on open and set one, then let's return the
3025 // focus to it and then remove our saved reference to it.
3026 if (this.$toggleButton && this.$toggleButton.length) {
3027 this.$toggleButton.focus().attr('aria-expanded', false);
3028 this.$toggleButton = null;
3029 }
3030
3031 utils.enableScrollBody();
3032 utils.disableTabbingOfChildren(this.$container);
3033 this.$container.attr('aria-hidden', true);
3034
3035 this.document().off(this.namespaceOpen);
3036
3037 $('body').removeClass(classes$9.drawerActive);
3038 this.trigger('drawer_close_transitioning');
3039
3040 utils.promiseTransitionEnd(this.$container).then(
3041 function() {
3042 this.trigger('drawer_close_done');
3043 }.bind(this)
3044 );
3045 },
3046
3047 _onWindowResize: function() {
3048 // Reset in case width changes on resize. Used for determining how much we
3049 // want to translate the page to the side. Translating === sliderWidth.
3050 this.sliderWidth = 0;
3051 },
3052
3053 _onTouchStart: function(event) {
3054 this.startPosition = utils.pointerEventToXY(event);
3055 this.$elementsToPush = this.$elementsToPush || $(selectors$9.drawerPush); // eslint-disable-line shopify/jquery-dollar-sign-reference
3056 this.sliderWidth = this.sliderWidth || this.$container.width();
3057 this.isDragging = false;
3058
3059 this.document().on(
3060 'touchmove' + this.namespaceTouch,
3061 this._onTouchMove.bind(this)
3062 );
3063 this.document().on(
3064 'touchend' + this.namespaceTouch,
3065 this._onTouchEnd.bind(this)
3066 );
3067 },
3068
3069 _onTouchMove: function(event) {
3070 var dx;
3071 var dy;
3072 var deg;
3073 var transform;
3074
3075 this.isDragging = true;
3076 this.currentPosition = utils.pointerEventToXY(event);
3077 this.trigger('drawer_dragging', [this.currentPosition]);
3078
3079 dx = this.currentPosition.x - this.startPosition.x;
3080 dy = this.currentPosition.y - this.startPosition.y;
3081 deg = Math.atan2(dx, dy) / (Math.PI / 180);
3082 this.translated = dx > 0 ? dx : 0;
3083
3084 if (
3085 Math.abs(deg) > 65 &&
3086 Math.abs(deg) < 115 &&
3087 this.draggingAction === null
3088 ) {
3089 this.draggingAction = 'slide';
3090 } else if (
3091 (Math.abs(deg) < 65 || Math.abs(deg) > 115) &&
3092 this.draggingAction === null
3093 ) {
3094 this.draggingAction = 'scroll';
3095 }
3096
3097 if (this.draggingAction === 'slide') {
3098 event.preventDefault();
3099
3100 transform = {
3101 transform:
3102 'translateX(-' + (this.sliderWidth - this.translated) + 'px)',
3103 transition: 'none'
3104 };
3105
3106 this.$elementsToPush.css(transform);
3107 }
3108 },
3109
3110 _onTouchEnd: function() {
3111 var percentMoved;
3112
3113 this.wasDragging = this.isDragging;
3114 this.isDragging = false;
3115
3116 if (this.wasDragging) {
3117 percentMoved = this.translated / this.sliderWidth;
3118 if (percentMoved > 0.25 && this.draggingAction === 'slide') {
3119 this.drawerClose('ease-out');
3120 this.trigger('drawer_dragging_success');
3121 } else {
3122 this.trigger('drawer_dragging_fail');
3123 }
3124
3125 this.$elementsToPush.removeAttr('style');
3126 }
3127
3128 this.draggingAction = null;
3129
3130 this.document().off(this.namespaceTouch);
3131 },
3132
3133 _closeOnEscape: function(evt) {
3134 if (evt.keyCode === 27) {
3135 this.drawerClose();
3136 }
3137 }
3138};
3139
3140/*
3141
3142Cart Drawer Section
3143--------------------------------------------------------------------------------
3144Creates a page drawer with AJAX cart functionilty
3145
3146*/
3147
3148// Extensions
3149// Libs
3150var selectors$7 = {
3151 drawer: '.drawer',
3152 cartHeader: '.cart-drawer__header',
3153 cartTitle: '.cart-drawer__header-title',
3154 cartContent: '.cart-drawer__content',
3155 cartToggle: '.ajax-cart__toggle',
3156 cartCloseButton: '.cart-drawer__close-button',
3157 siteHeader: '.site-header'
3158};
3159
3160var classes$7 = {
3161 cartTemplate: 'template-cart',
3162 cartEmpty: 'cart-drawer--empty',
3163 cartNoCookies: 'cart-drawer--no-cookies'
3164};
3165
3166sections.register('cart-drawer', {
3167 onLoad: function() {
3168 if ($(document.body).hasClass(classes$7.cartTemplate)) return;
3169
3170 // Setup the drawer functionality
3171 this.extend(drawer);
3172 this._setDrawerHeaderHeight();
3173 this.on('drawer_close_done', this._onDrawerClose.bind(this));
3174 this.document().on(
3175 'click',
3176 selectors$7.cartToggle,
3177 this._openCartDrawer.bind(this)
3178 );
3179
3180 // If cookies are disabled, then add a class which shows a warning and don't
3181 // continue any further. We don't want the AJAX Cart functionality.
3182 if (!utils.cookiesEnabled()) {
3183 this.$container.addClass(classes$7.cartNoCookies);
3184 return;
3185 }
3186
3187 this.extend(ajaxCart);
3188 cart.on(
3189 'change',
3190 function(event, state) {
3191 this.$ajaxCart.toggleClass(classes$7.cartEmpty, state.items.length === 0);
3192 }.bind(this)
3193 );
3194 },
3195
3196 onSelect: function() {
3197 this._openCartDrawer();
3198 },
3199
3200 onDeselect: function() {
3201 this._closeCartDrawer();
3202 },
3203
3204 _onDrawerClose: function() {
3205 sections.getInstances('header-section').then(function(instances) {
3206 instances[0].scrollHandler = true;
3207 });
3208 },
3209
3210 _setDrawerHeaderHeight: function() {
3211 var $cartHeader = $(selectors$7.cartHeader, this.$container);
3212 var siteHeaderHeight = $(selectors$7.siteHeader).outerHeight();
3213
3214 $cartHeader.css('height', siteHeaderHeight);
3215 },
3216
3217 _openCartDrawer: function(evt) {
3218 evt.preventDefault();
3219
3220 sections
3221 .getInstances('header-section')
3222 .then(function(instances) {
3223 instances[0].scrollHandler = false;
3224 return instances[0].hideNavigation();
3225 })
3226 .then(
3227 function() {
3228 this.drawerOpen(evt);
3229
3230 $(selectors$7.cartContent, this.$container).scrollTop(0);
3231
3232 a11y$1.forceFocus($(selectors$7.cartTitle, this.$container));
3233
3234 this.$container.on(
3235 'click',
3236 selectors$7.cartCloseButton,
3237 this._closeCartDrawer.bind(this)
3238 );
3239 }.bind(this)
3240 );
3241 },
3242
3243 _closeCartDrawer: function() {
3244 this.drawerClose();
3245 }
3246});
3247
3248/*
3249
3250Cart Template Section
3251--------------------------------------------------------------------------------
3252Adds ajax cart functionality to the cart template
3253
3254*/
3255
3256// Extensions
3257// Libs
3258var classes$10 = {
3259 showClass: 'cart-item__update--show',
3260 showEditClass: 'cart-item__edit--active',
3261 cartEmpty: 'cart--empty',
3262 cartNoCookies: 'cart--no-cookies'
3263};
3264
3265sections.register('cart-template', {
3266 onLoad: function() {
3267 var $container = $(this.container);
3268
3269 if (!utils.cookiesEnabled()) {
3270 $container.addClass(classes$10.cartNoCookies);
3271 return;
3272 }
3273
3274 this.extend(ajaxCart);
3275
3276 cart.on('change', function(event, state) {
3277 $container.toggleClass(classes$10.cartEmpty, state.items.length === 0);
3278 });
3279 }
3280});
3281
3282/*
3283
3284Collection Template Section
3285--------------------------------------------------------------------------------
3286Creates a staggered card grid for the collection template. Manages filters.
3287
3288*/
3289
3290// Extensions
3291// Libs
3292var selectors$10 = {
3293 filterBy: '#FilterBy',
3294 sortBy: '#SortBy',
3295 collectionImage: '.section-header-image'
3296};
3297var defaultSort = 'title-ascending';
3298var queryParams = {};
3299
3300sections.register('collection-template', {
3301 onLoad: function() {
3302 var $container = (this.$container = $(this.container));
3303 var $filterBy = $(selectors$10.filterBy, $container);
3304 var $sortBy = $(selectors$10.sortBy, $container);
3305
3306 this.defaultSort = this.getDefaultSortValue();
3307 this.extend(cardManager);
3308 this.initParams();
3309
3310 $filterBy.on('change', this.onFilterChange.bind(this));
3311 $sortBy.on('change', this.onSortChange.bind(this));
3312 },
3313
3314 initParams: function() {
3315 if (!location.search.length) return;
3316
3317 var aKeyValue;
3318 var aCouples = location.search.substr(1).split('&');
3319
3320 for (var i = 0; i < aCouples.length; i++) {
3321 aKeyValue = aCouples[i].split('=');
3322 if (aKeyValue.length > 1) {
3323 queryParams[decodeURIComponent(aKeyValue[0])] = decodeURIComponent(
3324 aKeyValue[1]
3325 );
3326 }
3327 }
3328 },
3329
3330 onSortChange: function() {
3331 queryParams.sort_by = this.getSortValue();
3332 if (queryParams.page) {
3333 delete queryParams.page;
3334 }
3335 location.search = decodeURIComponent($.param(queryParams));
3336 },
3337
3338 onFilterChange: function() {
3339 this.filter = this.getFilterValue();
3340 document.location.href = this.filter;
3341 },
3342
3343 getSortValue: function() {
3344 return $(selectors$10.sortBy).val() || this.defaultSort;
3345 },
3346
3347 getDefaultSortValue: function() {
3348 return $(selectors$10.sortBy, this.$container).val() || defaultSort;
3349 },
3350
3351 getFilterValue: function() {
3352 return $(selectors$10.filterBy, this.$container).val() || 'collections/all';
3353 }
3354});
3355
3356/*
3357
3358Featured Blog Post Section
3359--------------------------------------------------------------------------------
3360Creates a staggered card grid for the featured blog post section.
3361
3362*/
3363
3364// Extensions
3365// Libs
3366sections.register('featured-blog', {
3367 onLoad: function() {
3368 this.extend(cardManager);
3369 }
3370});
3371
3372var product = {
3373 validate: function(product) {
3374 if (typeof product !== 'object' || typeof product.id !== 'number') {
3375 throw Error(
3376 'Please pass a valid Product object to the Product Controller'
3377 );
3378 }
3379
3380 return $.extend({}, product);
3381 },
3382
3383 getVariant: function(product, value) {
3384 var variant;
3385
3386 if (typeof value === 'string' || typeof value === 'number') {
3387 // If value is an id
3388 variant = this._getVariantFromId(product, value);
3389 } else if (typeof value === 'object' && typeof value.id === 'number') {
3390 // If value is a variant object containing an id key
3391 variant = this._getVariantFromId(product, value.id);
3392 } else if (isArray(value)) {
3393 // If value is an array of options
3394 if (typeof value[0] === 'object') {
3395 // If value is a collection of options with name and value keys
3396 variant = this._getVariantFromOptionCollection(product, value);
3397 } else {
3398 // If value is an array of option values, ordered by index of array
3399 variant = this._getVariantFromOptionArray(product, value);
3400 }
3401 }
3402
3403 return variant;
3404 },
3405
3406 optionArrayFromOptionCollection: function(product, collection) {
3407 var optionArray = [];
3408
3409 collection.forEach(function(option) {
3410 var index;
3411
3412 if (typeof option.name !== 'string') {
3413 throw Error(
3414 'Invalid value type passed for name of option ' +
3415 index +
3416 '. Value should be string.'
3417 );
3418 }
3419
3420 index = findIndex(product.options, function(name) {
3421 return name.toLowerCase() === option.name.toLowerCase();
3422 });
3423
3424 if (index === -1) {
3425 throw Error('Invalid option name, ' + option.name);
3426 }
3427
3428 optionArray[index] = option.value;
3429 });
3430
3431 return optionArray;
3432 },
3433
3434 _getVariantFromId: function(product, id) {
3435 return find(product.variants, { id: id });
3436 },
3437
3438 _getVariantFromOptionCollection: function(product, collection, closest) {
3439 var optionArray = this.optionArrayFromOptionCollection(product, collection);
3440
3441 return this._getVariantFromOptionArray(product, optionArray, closest);
3442 },
3443
3444 _getVariantFromOptionArray: function(product, options) {
3445 return find(product.variants, function(variant) {
3446 return options.every(function(option, index) {
3447 return variant.options[index] === option;
3448 });
3449 });
3450 }
3451};
3452
3453/*
3454
3455Product Display Extension
3456--------------------------------------------------------------------------------
3457Manages the state of the product selection display.
3458
3459
3460Events
3461------------
3462
3463Name: variant_change
3464Description: Fired whenever a new variant is selected
3465Payload: { object } Product object of the item being selected
3466Payload: { object } Variant object resulted from options selection
3467
3468Name: variant_change_undefined
3469Description: Fired when the product options selected result in a undefined variant
3470Payload: { object } Product object of the item being selected
3471Payload: { array } Collection of form option input names and values
3472
3473Name: variant_change_successful
3474Description: Fired when the product options selected result in a variant
3475Payload: { object } Product object of the item being selected
3476Payload: { object } Variant object resulted from options selection
3477
3478Name: variant_add_to_cart
3479Description: Fired when a product variant is added to the cart
3480Payload: { object } Product object of the item being added to the cart
3481Payload: { object } Variant object of the item being added to the cart
3482Payload: { object } Serialized form data being submitted to the cart API
3483
3484Name: variant_add_to_cart_successful
3485Description: Fired when a product variant has been added to the cart successfully
3486Payload: { object } Product object of the item being added to the cart
3487Payload: { object } Variant object of the item being added to the cart
3488Payload: { object } Serialized form data being submitted to the cart API
3489
3490Name: variant_add_to_cart_fail
3491Description: Fired when a product variant has failed to be added to the cart
3492Payload: { object } Product object of the item being added to the cart
3493Payload: { object } Variant object of the item being added to the cart
3494Payload: { string } Error string
3495
3496*/
3497
3498var classes$11 = {
3499 productPriceSale: 'product__price--sale',
3500 productPriceUnitUnavailable: 'product-price-unit--unavailable',
3501 productNotificationSuccess: 'product__notification--success',
3502 productNotificationError: 'product__notification--error',
3503 buttonTransition: 'btn--to-secondary-transitioned',
3504 ajaxCartToggle: 'ajax-cart__toggle',
3505 hide: 'hide',
3506 lazyPreload: 'lazypreload'
3507};
3508
3509var selectors$12 = {
3510 productForm: '.product-form',
3511 selectorWrapper: '.product-form__item',
3512 ajaxCartToggle: '.ajax-cart__toggle',
3513 shopifyPaymentButton: '.shopify-payment-button',
3514
3515 productJSON: '[data-product-json]',
3516 optionInputs: '[data-option-input]',
3517 masterSelect: '[data-master-select]',
3518 variantImage: '[data-variant-image]',
3519 variantImageToggleHide: '[data-variant-image-toggle-hide]',
3520 variantImageSrc: '[data-variant-image-src]',
3521 productPrice: '[data-product-price]',
3522 regularPrice: '[data-regular-price]',
3523 compareAtPrice: '[data-compare-price]',
3524 unitPrice: '[data-unit-price]',
3525 unitPriceBaseUnit: '[data-unit-price-base-unit]',
3526 unitPriceContainer: '[data-unit-price-container]',
3527 submitButton: '[data-cart-submit]',
3528 submitButtonPrimaryText: '[data-cart-primary-submit-text]',
3529 submitButtonSecondaryText: '[data-cart-secondary-submit-text]',
3530 notification: '[data-cart-notification]'
3531};
3532
3533var productDisplay = {
3534 init: function() {
3535 var $productJSON = $(selectors$12.productJSON, this.container);
3536
3537 if ($productJSON.length === 0) return;
3538
3539 this.product = JSON.parse($productJSON.html());
3540 this.variant = product.getVariant(
3541 this.product,
3542 this.$container.data('variant-id')
3543 );
3544
3545 this._formatVariantSelectors();
3546
3547 this.on('submit', selectors$12.productForm, this._addItemToCart.bind(this));
3548 this.one(
3549 'focus',
3550 selectors$12.optionInputs,
3551 this._preloadVariantImages.bind(this)
3552 );
3553 this.on(
3554 'change.variantController',
3555 selectors$12.optionInputs,
3556 this.update.bind(this)
3557 );
3558
3559 this.document().on(
3560 'click',
3561 selectors$12.ajaxCartToggle,
3562 this._resetAddToCartButton.bind(this)
3563 );
3564
3565 this.window().on('online', this._updateOnlineStatus);
3566 this.window().on('offline', this._updateOfflineStatus);
3567 },
3568
3569 update: function() {
3570 var $inputs = $(selectors$12.optionInputs, this.$container);
3571 var options = $inputs.serializeArray();
3572 var variant = product.getVariant(this.product, options);
3573
3574 // If the variant we tried to find with the selected options does not exist
3575 // then modify and clone the currently selected variant and transform it
3576 // to an 'unavailable' product variant object.
3577 if (typeof variant === 'undefined') {
3578 this.trigger('variant_change_undefined', [this.product, options]);
3579
3580 variant = $.extend({}, this.variant); // Clone variant because we are going to edit it
3581 variant.isUndefined = true;
3582 variant.available = false;
3583 variant.options = product.optionArrayFromOptionCollection(
3584 this.product,
3585 options
3586 );
3587 } else {
3588 this.trigger('variant_change_successful', [this.product, variant]);
3589 }
3590
3591 this.variant = variant;
3592
3593 // MorphDOM - Here is the old container, and the new container, only update
3594 // the DOM with things that have changed.
3595 morphdom(this.$container[0], this._updatedContainer(this.variant));
3596
3597 this.trigger('variant_change', [this.product, variant]);
3598 },
3599
3600 _preloadVariantImages: function() {
3601 $(selectors$12.variantImage, this.$container).addClass(classes$11.lazyPreload);
3602 },
3603
3604 // The padding on the left side of the variant select's needs to be set
3605 // to the same width as the option label. This can only be done when the main
3606 // stylesheet has been downloaded, so we use utils.promiseStylesheet()
3607 _formatVariantSelectors: function() {
3608 utils.promiseStylesheet().then(
3609 function() {
3610 $(selectors$12.selectorWrapper, this.$container).each(function() {
3611 var $wrapper = $(this);
3612 var $label = $wrapper.find('label');
3613 var $input = $wrapper.find('select, input');
3614
3615 $input.css({
3616 'padding-left': $label.outerWidth(),
3617 opacity: 1
3618 });
3619 });
3620 }.bind(this)
3621 );
3622 },
3623
3624 // Clone the current version of the container and update elements with jQuery
3625 // according to the new variant values. This is an alternative to having a
3626 // duplicate Handlebars template that you would pass the cart object to
3627 // generate the updated container.
3628 _updatedContainer: function(variant) {
3629 var $container = this.$container.clone();
3630
3631 $(selectors$12.masterSelect, $container).val(variant.id);
3632
3633 this._updateInputValues(variant, $container);
3634 this._updateProductPrices(variant, $container);
3635 this._updateVariantImage(variant, $container);
3636 this._toggleVariantImageHide(variant, $container);
3637 this._updateCartButtonState(variant, $container);
3638
3639 return $container[0];
3640 },
3641
3642 _updateInputValues: function(variant, $container) {
3643 $(selectors$12.optionInputs, $container).each(function(index) {
3644 $(this).val(variant.options[index]);
3645 });
3646 },
3647
3648 _updateCartButtonState: function(variant, $container) {
3649 var text;
3650
3651 if (variant.isUndefined) {
3652 text = theme.strings.unavailable;
3653 } else {
3654 text = variant.available
3655 ? theme.strings.addToCart
3656 : theme.strings.soldOut;
3657 }
3658
3659 this._resetAddToCartButton($container);
3660
3661 if (variant.available) {
3662 $(selectors$12.shopifyPaymentButton, $container).show();
3663 } else {
3664 $(selectors$12.shopifyPaymentButton, $container).hide();
3665 }
3666
3667 $(selectors$12.submitButton, $container)
3668 .prop('disabled', !variant.available)
3669 .attr('aria-label', text);
3670
3671 $(selectors$12.submitButtonPrimaryText, $container).text(text);
3672 },
3673
3674 _updateProductPrices: function(variant, $container) {
3675 var productPrice = variant.price;
3676 var comparePrice = variant.compare_at_price;
3677
3678 $(selectors$12.regularPrice, $container).html(
3679 currency.formatMoney(productPrice, theme.moneyFormat)
3680 );
3681
3682 $(selectors$12.compareAtPrice, $container).html(
3683 currency.formatMoney(comparePrice, theme.moneyFormat)
3684 );
3685
3686 $(selectors$12.productPrice, $container).toggleClass(
3687 classes$11.productPriceSale,
3688 comparePrice > productPrice
3689 );
3690
3691 $(selectors$12.unitPriceContainer, $container).addClass(
3692 classes$11.productPriceUnitUnavailable
3693 );
3694 if (variant.unit_price_measurement) {
3695 $(selectors$12.unitPrice, $container).html(
3696 currency.formatMoney(variant.unit_price, theme.moneyFormat)
3697 );
3698 $(selectors$12.unitPriceBaseUnit, $container).html(
3699 this.getBaseUnit(variant)
3700 );
3701 $(selectors$12.unitPriceContainer, $container).removeClass(
3702 classes$11.productPriceUnitUnavailable
3703 );
3704 }
3705 },
3706
3707 _updateVariantImage: function(variant, $container) {
3708 var src =
3709 (variant.featured_image && variant.featured_image.src) ||
3710 this.product.featured_image;
3711 var $variantImage = $(selectors$12.variantImageSrc, $container);
3712 var size = images.imageSize($variantImage.attr('src'));
3713 var sizedImgUrl = images.getSizedImageUrl(src, size);
3714
3715 $variantImage.attr('src', sizedImgUrl);
3716 },
3717
3718 _toggleVariantImageHide: function(variant, $container) {
3719 if (!variant.featured_image && !this.product.featured_image) return;
3720
3721 var image = variant.featured_image || this.product.featured_image.id;
3722 var id = image && image.id;
3723 var $images = $(selectors$12.variantImageToggleHide, $container);
3724 var $active = $images.filter('[data-id="' + id + '"]');
3725
3726 $active
3727 .removeClass(classes$11.hide)
3728 .siblings()
3729 .addClass(classes$11.hide);
3730 },
3731
3732 _addItemToCart: function(evt) {
3733 if (!sections.isInstance('cart-drawer')) return;
3734
3735 evt.preventDefault();
3736 var $button = $(selectors$12.submitButton, this.$container);
3737 var data = new FormData(evt.target);
3738 if ($button.hasClass(classes$11.ajaxCartToggle)) return;
3739
3740 cart
3741 .addItemFromForm(data)
3742 .then(this._transitionAddToCartButton.bind(this, data))
3743 .catch(this._onAddItemFail.bind(this))
3744 .always(
3745 function() {
3746 this.trigger('variant_add_to_cart', [
3747 this.product,
3748 this.variant,
3749 data
3750 ]);
3751 }.bind(this)
3752 );
3753 },
3754
3755 _onAddItemFail: function(response) {
3756 var $notification = $(selectors$12.notification, this.$container);
3757 var responseText = response && response.responseText;
3758
3759 if (responseText) {
3760 responseText = JSON.parse(responseText).description;
3761 } else {
3762 responseText = theme.strings.cartError;
3763 }
3764
3765 $notification
3766 .addClass(classes$11.productNotificationError)
3767 .removeClass(classes$11.productNotificationSuccess)
3768 .html(responseText)
3769 .attr('role', 'alert');
3770
3771 this.trigger('variant_add_to_cart_fail', [
3772 this.product,
3773 this.variant,
3774 responseText
3775 ]);
3776
3777 return utils.promiseTransitionEnd($notification);
3778 },
3779
3780 _transitionAddToCartButton: function(data) {
3781 var $notification = $(selectors$12.notification, this.$container);
3782 var $button = $(selectors$12.submitButton, this.$container);
3783 var $primaryButtonText = $(selectors$12.submitButtonPrimaryText, $button);
3784 var $secondaryButtonText = $(selectors$12.submitButtonSecondaryText, $button);
3785
3786 $button
3787 .addClass([classes$11.buttonTransition, classes$11.ajaxCartToggle].join(' '))
3788 .attr('aria-label', theme.strings.viewCart);
3789 $primaryButtonText.attr('aria-hidden', true);
3790 $secondaryButtonText.attr('aria-hidden', false);
3791
3792 $notification
3793 .removeClass(classes$11.productNotificationError)
3794 .addClass(classes$11.productNotificationSuccess)
3795 .text(theme.strings.addItemSuccess)
3796 .attr('role', 'alert');
3797
3798 this.trigger('variant_add_to_cart_successful', [
3799 this.product,
3800 this.variant,
3801 data
3802 ]);
3803
3804 return utils.promiseTransitionEnd($notification);
3805 },
3806
3807 _resetAddToCartButton: function($container) {
3808 $container = $container.length ? $container : this.$container;
3809 var $notification = $(selectors$12.notification, $container);
3810 var $button = $(selectors$12.submitButton, $container);
3811 var $primaryButtonText = $(selectors$12.submitButtonPrimaryText, $button);
3812 var $secondaryButtonText = $(selectors$12.submitButtonSecondaryText, $button);
3813
3814 $button
3815 .removeClass([classes$11.buttonTransition, classes$11.ajaxCartToggle].join(' '))
3816 .attr('aria-label', theme.strings.addToCart);
3817 $primaryButtonText.attr('aria-hidden', false);
3818 $secondaryButtonText.attr('aria-hidden', true);
3819
3820 $notification
3821 .removeClass(classes$11.productNotificationError)
3822 .removeClass(classes$11.productNotificationSuccess)
3823 .text('')
3824 .attr('role', '');
3825 },
3826
3827 _updateOnlineStatus: function() {
3828 theme.isOnline = true;
3829 },
3830
3831 _updateOfflineStatus: function() {
3832 theme.isOnline = false;
3833 },
3834
3835 getBaseUnit: function(variant) {
3836 return variant.unit_price_measurement.reference_value === 1
3837 ? variant.unit_price_measurement.reference_unit
3838 : variant.unit_price_measurement.reference_value +
3839 variant.unit_price_measurement.reference_unit;
3840 }
3841};
3842
3843/*
3844
3845Featured Product Section
3846--------------------------------------------------------------------------------
3847Shows a product and allows the user to select product variants and add the product
3848to their cart.
3849
3850*/
3851
3852// Extensions
3853// Libs
3854var selectors$11 = {
3855 socialSharing: '.social-sharing',
3856 detailsButton: '.product__more-details'
3857};
3858
3859sections.register('featured-product', {
3860 onLoad: function() {
3861 this.extend(socialSharing);
3862 this.extend(productDisplay);
3863 this.on('variant_change', this._updateProductLink.bind(this));
3864
3865 this.window().on(
3866 'resize',
3867 debounce(this._formatVariantSelectors.bind(this), 500)
3868 );
3869 },
3870
3871 _updateProductLink: function(evt, instance, product, variant) {
3872 var $link = $(selectors$11.detailsButton, this.$container);
3873 var url = utils.updateUrlParameter(
3874 $link.attr('href'),
3875 'variant',
3876 variant.id
3877 );
3878
3879 $link.attr('href', url);
3880 }
3881});
3882
3883var selectors$14 = {
3884 slides: '.slider__slide',
3885 activeSlide: '.slider__slide--active'
3886};
3887
3888var classes$13 = {
3889 activeSlide: 'slider__slide--active',
3890 previousSlide: 'slider__slide--previous',
3891 nextSlide: 'slider__slide--next',
3892 transitioningSlide: 'slider__slide--transitioning',
3893 draggingSlide: 'slider__slide--dragging'
3894};
3895
3896var events = {
3897 nextSlide: 'nextslide',
3898 prevSlide: 'prevslide',
3899 transitionStart: 'slidetransitionstart',
3900 transitionEnd: 'slidetransitionend'
3901};
3902
3903var defaults = {
3904 moveThreshold: 25
3905};
3906
3907function Slider(element, settings) {
3908 this.$element = $(element);
3909 this.$slides = $(selectors$14.slides, this.$element);
3910 this.config = $.extend({}, defaults, settings);
3911
3912 this._setCurrentSlide($(selectors$14.activeSlide, this.$element));
3913 this._assignTouchHandlers();
3914
3915 // We need to store the binded version of each event handler so that we can
3916 // remove it in the `slider.destroy()` method.
3917 this.bindedOnFocus = this._onFocus.bind(this);
3918 this.bindedOnBlur = this._onBlur.bind(this);
3919
3920 // Need useCapture parameter in .addEventListener (the last parameter, set here
3921 // to true) so we can't use jQuery `.on()`
3922 if (this.$slides.length) {
3923 this.$slides[0].addEventListener('focus', this.bindedOnFocus, true);
3924 this.$slides[0].addEventListener('blur', this.bindedOnBlur, true);
3925 }
3926}
3927
3928Slider.prototype = $.extend({}, Slider.prototype, {
3929 // Public Methods
3930 // ---------------------------------------------------------------------------
3931 destroy: function() {
3932 this.$element.off('touchstart mousedown');
3933 if (this.$slides.length) {
3934 this.$slides[0].removeEventListener('focus', this.bindedOnFocus, true);
3935 this.$slides[0].removeEventListener('blur', this.bindedOnBlur, true);
3936 }
3937
3938 return null;
3939 },
3940
3941 nextSlide: function() {
3942 this._triggerEvent([events.nextSlide, events.transitionStart]);
3943
3944 return this._promiseNextSlide().then(
3945 function() {
3946 this._triggerEvent(events.transitionEnd);
3947 }.bind(this)
3948 );
3949 },
3950
3951 previousSlide: function() {
3952 this._triggerEvent([events.prevSlide, events.transitionStart]);
3953
3954 return this._promisePrevSlide().then(
3955 function() {
3956 this._triggerEvent(events.transitionEnd);
3957 }.bind(this)
3958 );
3959 },
3960
3961 setSlide: function(setIndex, animate) {
3962 var slidePromiseChain = this.slidePromiseChain || $.Deferred().resolve();
3963 var currentIndex = this.$currentSlide.index();
3964 var totalSlides = this.$slides.length;
3965 var promiseSlide = $.Deferred().resolve();
3966
3967 this.slidePromiseChain = slidePromiseChain.then(
3968 function() {
3969 if (setIndex < totalSlides && setIndex !== currentIndex) {
3970 if (animate) {
3971 if (setIndex > currentIndex) {
3972 promiseSlide = utils.promiseRepeatSeries(
3973 this.nextSlide.bind(this),
3974 setIndex - currentIndex
3975 );
3976 } else if (setIndex < currentIndex) {
3977 promiseSlide = utils.promiseRepeatSeries(
3978 this.previousSlide.bind(this),
3979 currentIndex - setIndex
3980 );
3981 }
3982 } else {
3983 promiseSlide = this._promiseSlide(this.$slides.eq(setIndex));
3984 }
3985 }
3986
3987 return promiseSlide;
3988 }.bind(this)
3989 );
3990
3991 return this.slidePromiseChain;
3992 },
3993
3994 // Slide Management
3995 // ---------------------------------------------------------------------------
3996 _promiseNextSlide: function() {
3997 requestAnimationFrame(
3998 function() {
3999 this.$nextSlide.addClass(classes$13.nextSlide);
4000
4001 requestAnimationFrame(
4002 function() {
4003 this.$slides.removeAttr('style');
4004
4005 this.$nextSlide
4006 .removeClass(classes$13.nextSlide)
4007 .addClass(classes$13.activeSlide)
4008 .addClass(classes$13.transitioningSlide);
4009
4010 this.$currentSlide
4011 .removeClass(classes$13.activeSlide)
4012 .addClass(classes$13.previousSlide)
4013 .addClass(classes$13.transitioningSlide);
4014 }.bind(this)
4015 );
4016 }.bind(this)
4017 );
4018
4019 return this._promiseSlideTransitionEnd().then(
4020 function() {
4021 this._setCurrentSlide(this.$nextSlide);
4022 }.bind(this)
4023 );
4024 },
4025
4026 _promisePrevSlide: function() {
4027 requestAnimationFrame(
4028 function() {
4029 this.$previousSlide.addClass(classes$13.previousSlide);
4030
4031 requestAnimationFrame(
4032 function() {
4033 this.$slides.removeAttr('style');
4034
4035 this.$previousSlide
4036 .removeClass(classes$13.previousSlide)
4037 .addClass(classes$13.activeSlide)
4038 .addClass(classes$13.transitioningSlide);
4039
4040 this.$currentSlide
4041 .removeClass(classes$13.activeSlide)
4042 .addClass(classes$13.nextSlide)
4043 .addClass(classes$13.transitioningSlide);
4044 }.bind(this)
4045 );
4046 }.bind(this)
4047 );
4048
4049 return this._promiseSlideTransitionEnd().then(
4050 function() {
4051 this._setCurrentSlide(this.$previousSlide);
4052 }.bind(this)
4053 );
4054 },
4055
4056 _promiseSlide: function($slide) {
4057 this._setCurrentSlide($slide);
4058
4059 $slide
4060 .addClass(classes$13.activeSlide)
4061 .siblings()
4062 .removeClass(classes$13.activeSlide);
4063
4064 return $.Deferred(
4065 function(defer) {
4066 requestAnimationFrame(
4067 function() {
4068 this._triggerEvent(events.transitionEnd);
4069 defer.resolve();
4070 }.bind(this)
4071 );
4072 }.bind(this)
4073 );
4074 },
4075
4076 _promiseCancelSlide: function() {
4077 requestAnimationFrame(
4078 function() {
4079 this.$slides.removeAttr('style').addClass(classes$13.transitioningSlide);
4080 }.bind(this)
4081 );
4082
4083 return this._promiseSlideTransitionEnd();
4084 },
4085
4086 _promiseSlideTransitionEnd: function() {
4087 return $.Deferred(
4088 function(defer) {
4089 this.$currentSlide.one(
4090 'transitionend',
4091 function() {
4092 this.$slides.removeClass(
4093 [
4094 classes$13.transitioningSlide,
4095 classes$13.nextSlide,
4096 classes$13.previousSlide
4097 ].join(' ')
4098 );
4099 defer.resolve();
4100 }.bind(this)
4101 );
4102 }.bind(this)
4103 );
4104 },
4105
4106 _setCurrentSlide: function($slide) {
4107 this.$currentSlide = $slide;
4108 this.$nextSlide = this._nextSlideIndex($slide, this.$slides);
4109 this.$previousSlide = this._prevSlideIndex($slide, this.$slides);
4110 },
4111
4112 // Touch Handlers
4113 // ---------------------------------------------------------------------------
4114
4115 _assignTouchHandlers: function() {
4116 this.bindedTouchStart = this._onTouchStart.bind(this);
4117 this.bindedTouchMove = this._onTouchMove.bind(this);
4118 this.bindedTouchEnd = this._onTouchEnd.bind(this);
4119
4120 // When a touchstart OR mousedown event is fired, take action.
4121 this.$element
4122 .one('touchstart', this.bindedTouchStart)
4123 .one('mousedown', this.bindedTouchStart);
4124 },
4125
4126 _onTouchStart: function(evt) {
4127 this.windowWidth = $(window).innerWidth();
4128 this.startPosition = utils.pointerEventToXY(evt);
4129 this.isDragging = false;
4130
4131 this.$element
4132 .on('touchmove mousemove', this.bindedTouchMove)
4133 .on('touchend mouseup', this.bindedTouchEnd);
4134 },
4135
4136 _onTouchMove: function(evt) {
4137 this.currentPosition = utils.pointerEventToXY(evt);
4138 this.percentMoved =
4139 ((this.currentPosition.x - this.startPosition.x) / this.windowWidth) *
4140 100;
4141
4142 if (!this.isDragging) {
4143 this.isDragging = true;
4144 }
4145
4146 if (this.percentMoved > 0) {
4147 this.$previousSlide.addClass(classes$13.previousSlide);
4148 this.$nextSlide.removeClass(classes$13.nextSlide);
4149 this.$previousSlide.css({
4150 transform: 'translateX(' + (this.percentMoved - 100) + '%)'
4151 });
4152 } else {
4153 this.$previousSlide.removeClass(classes$13.previousSlide);
4154 this.$nextSlide.addClass(classes$13.nextSlide);
4155 this.$nextSlide.css({
4156 transform: 'translateX(' + (100 + this.percentMoved) + '%)'
4157 });
4158 }
4159
4160 this.$currentSlide
4161 .addClass(classes$13.draggingSlide)
4162 .css({ transform: 'translateX(' + this.percentMoved + '%)' });
4163 },
4164
4165 _onTouchEnd: function() {
4166 var moveThreshold = this.config.moveThreshold;
4167 var promiseTransition = $.Deferred().resolve();
4168
4169 this.$element.off();
4170
4171 if (this.isDragging) {
4172 this.isDragging = false;
4173
4174 this.$currentSlide.removeClass(classes$13.draggingSlide);
4175
4176 if (this.percentMoved > moveThreshold) {
4177 promiseTransition = this.previousSlide();
4178 } else if (this.percentMoved < -moveThreshold) {
4179 promiseTransition = this.nextSlide();
4180 } else {
4181 promiseTransition = this._promiseCancelSlide();
4182 }
4183 }
4184
4185 promiseTransition.then(
4186 function() {
4187 this.$element
4188 .one('touchstart', this.bindedTouchStart)
4189 .one('mousedown', this.bindedTouchStart);
4190 }.bind(this)
4191 );
4192 },
4193
4194 // A11y
4195 // ---------------------------------------------------------------------------
4196 _onFocus: function() {
4197 this.$element.one('keyup', this._onKeyup.bind(this));
4198 },
4199
4200 _onBlur: function() {
4201 this.$element.off('keyup');
4202 },
4203
4204 _onKeyup: function(evt) {
4205 var promiseTransition = $.Deferred().resolve();
4206 var refocusOnSlideShow = false;
4207
4208 switch (evt.keyCode) {
4209 case 37:
4210 promiseTransition = this.previousSlide();
4211 refocusOnSlideShow = $.contains(
4212 this.$element[0],
4213 document.activeElement
4214 );
4215 break;
4216 case 39:
4217 promiseTransition = this.nextSlide();
4218 refocusOnSlideShow = $.contains(
4219 this.$element[0],
4220 document.activeElement
4221 );
4222 break;
4223 }
4224
4225 promiseTransition.then(
4226 function() {
4227 if (refocusOnSlideShow) {
4228 this.$element.focus();
4229 }
4230 this.$element.one('keyup', this._onKeyup.bind(this));
4231 }.bind(this)
4232 );
4233 },
4234
4235 // Misc
4236 // ---------------------------------------------------------------------------
4237 _triggerEvent: function(names) {
4238 if (typeof names === 'string') {
4239 names = [names];
4240 }
4241
4242 names.forEach(
4243 function(name) {
4244 this.$element.trigger(name, {
4245 $previousSlide: this.$previousSlide,
4246 $currentSlide: this.$currentSlide,
4247 $nextSlide: this.$nextSlide
4248 });
4249 }.bind(this)
4250 );
4251 },
4252
4253 _nextSlideIndex: function($current, $slides) {
4254 var index = $current.index();
4255 var count = $slides.length;
4256 var next = index + 1;
4257
4258 if (index + 1 === count) {
4259 next = 0;
4260 }
4261
4262 return $slides.eq(next);
4263 },
4264
4265 _prevSlideIndex: function($current, $slides) {
4266 var index = $current.index();
4267 var count = $slides.length;
4268 var prev = index - 1;
4269
4270 if (index - 1 === -1) {
4271 prev = count - 1;
4272 }
4273
4274 return $slides.eq(prev);
4275 }
4276});
4277
4278/*
4279
4280Reveal Slider Extension
4281--------------------------------------------------------------------------------
4282Manage the mobile reveal slider functionality.
4283
4284
4285Events
4286------------
4287Name: reveal_slider_init_start
4288Description: Fired at the start of the reveal slider initialization
4289Payload: none
4290
4291Name: reveal_slider_init_done
4292Description: Fired at the end of the reveal slider initialization
4293Payload: none
4294
4295Name: reveal_slider_destroy
4296Description: Fired when the reveal slider is destroyed
4297Payload: none
4298
4299Name: reveal_slider_open_cover
4300Description: Fired when the slider cover is opened
4301Payload: none
4302
4303Name: reveal_slider_close_cover
4304Description: Fired when the slider cover is closed
4305Payload: none
4306
4307Name: reveal_slider_set_slide
4308Description: Fired when the slider nav buttons are clicked to change a slide
4309Payload: { number } Index of the slide to set as active
4310
4311*/
4312
4313var selectors$13 = {
4314 coverTopBackground: '.reveal-slider__cover-background--top',
4315 coverBottomBackground: '.reveal-slider__cover-background--bottom',
4316 coverContent: '.reveal-slider__cover-content',
4317 slideNavButtons: '.reveal-slider__nav-button',
4318 slidePreviousButton: '.reveal-slider__previous-button',
4319 slideNextButton: '.reveal-slider__next-button',
4320 closeCoverButton: '.reveal-slider__close',
4321 slider: '.slider'
4322};
4323
4324var classes$12 = {
4325 navButtonActive: 'active',
4326 coverOpen: 'open'
4327};
4328
4329var revealSlider = {
4330 initRevealSlider: function() {
4331 this.trigger('reveal_slider_init_start');
4332
4333 this.revealSlider = true;
4334 this.coverIsOpen = false;
4335 this.revealSliderNamespace = '.revealSlider';
4336 this.mediaQuerySmall = theme.mediaQuerySmall;
4337
4338 this.slider = new Slider($(selectors$13.slider, this.$container));
4339 this.hammertime = new Hammer(this.$container[0]);
4340
4341 this.on(
4342 'click',
4343 selectors$13.slideNavButtons,
4344 this._onClickNavButton.bind(this)
4345 );
4346 this.on(
4347 'click',
4348 selectors$13.slidePreviousButton,
4349 this.slider.previousSlide.bind(this.slider)
4350 );
4351 this.on(
4352 'click',
4353 selectors$13.slideNextButton,
4354 this.slider.nextSlide.bind(this.slider)
4355 );
4356 this.on('click', selectors$13.closeCoverButton, this.closeCover.bind(this));
4357 this.on('click', selectors$13.coverContent, this.openCover.bind(this));
4358 this.on('nextslide prevslide', this._changeBackgroundColor.bind(this));
4359 this.on('slidetransitionend', this._onSlideTransitionEnd.bind(this));
4360
4361 this.document().on(
4362 'touchstart' + this.revealSliderNamespace,
4363 this._onTouchStart.bind(this)
4364 );
4365 this.document().on(
4366 'touchend' + this.revealSliderNamespace,
4367 this._onTouchEnd.bind(this)
4368 );
4369
4370 this.hammertime
4371 .on('pinchout pinchin', this._onPinch.bind(this))
4372 .on('pinchend pinchcancel', this._onPinchEnd.bind(this));
4373
4374 this.trigger('reveal_slider_init_done');
4375 },
4376
4377 destroyRevealSlider: function() {
4378 this.revealSlider = false;
4379 this.slider = this.slider.destroy();
4380 this.document().off(this.revealSliderNamespace);
4381 this.off();
4382
4383 this.trigger('reveal_slider_destroy');
4384 },
4385
4386 openCover: function() {
4387 this.$container.addClass(classes$12.coverOpen);
4388 this.coverIsOpen = true;
4389 this.trigger('reveal_slider_open_cover');
4390 },
4391
4392 closeCover: function() {
4393 this.$container.removeClass(classes$12.coverOpen);
4394 this.coverIsOpen = false;
4395 this.trigger('reveal_slider_close_cover');
4396 },
4397
4398 _changeBackgroundColor: function(evt, data) {
4399 var color;
4400
4401 if (evt.type === 'nextslide') {
4402 color = data.$nextSlide.data('background-color');
4403 } else {
4404 color = data.$previousSlide.data('background-color');
4405 }
4406
4407 $(selectors$13.slider, this.$container).css({ backgroundColor: color });
4408 },
4409
4410 _onSlideTransitionEnd: function(evt, data) {
4411 var index = data.$currentSlide.index();
4412
4413 $(selectors$13.slideNavButtons, this.$container)
4414 .eq(index)
4415 .addClass(classes$12.navButtonActive)
4416 .siblings()
4417 .removeClass(classes$12.navButtonActive);
4418 },
4419
4420 _onClickNavButton: function(evt) {
4421 var $navButton = $(evt.target);
4422 var index = $navButton.index();
4423 this.slider.setSlide($navButton.index());
4424 this.trigger('reveal_slider_set_slide', [index]);
4425 },
4426
4427 _onTouchStart: function(evt) {
4428 if (evt.originalEvent.touches.length <= 1) return;
4429
4430 this.enablePinch = true;
4431
4432 // Lock Scrolling over the zoom element by allowing Hammer.js to fire pinch events.
4433 this.hammertime.get('pinch').set({ enable: this.enablePinch });
4434 },
4435
4436 _onTouchEnd: function() {
4437 if (!this.enablePinch) return;
4438
4439 this.enablePinch = false;
4440 this.hammertime.get('pinch').set({ enable: this.enablePinch });
4441 },
4442
4443 _onPinch: function(evt) {
4444 this.$coverTopBackground =
4445 this.$coverTopBackground ||
4446 $(selectors$13.coverTopBackground, this.$container); // eslint-disable-line shopify/jquery-dollar-sign-reference
4447 this.$coverBottomBackground =
4448 this.$coverBottomBackground ||
4449 $(selectors$13.coverBottomBackground, this.$container); // eslint-disable-line shopify/jquery-dollar-sign-reference
4450 this.$coverTitle =
4451 this.$coverTitle || $(selectors$13.coverContent, this.$container); // eslint-disable-line shopify/jquery-dollar-sign-reference
4452
4453 if (
4454 (evt.type === 'pinchin' && !this.coverIsOpen) ||
4455 (evt.type === 'pinchout' && this.coverIsOpen)
4456 )
4457 return;
4458
4459 // The distance of the top and bottom backgrounds (relative to the center
4460 // of the slider) will be calculated based on evt.scale, which represents
4461 // the scale factor between two pointersets (two finger points).
4462 this.distance = this.coverIsOpen ? 70 * evt.scale : (evt.scale - 1) * 25;
4463
4464 if (this.distance < 0) {
4465 this.distance = 0;
4466 }
4467
4468 this.$coverBottomBackground.css({
4469 transform: 'translateY(' + this.distance + '%)'
4470 });
4471 this.$coverTopBackground.css({
4472 transform: 'translateY(-' + this.distance + '%)'
4473 });
4474 this.$coverTitle.css({ opacity: 1 - this.distance / 30 });
4475 },
4476
4477 // If the scale factor is greater than 2, the cover will open automatically.
4478 _onPinchEnd: function(evt) {
4479 if (evt.scale > 2) {
4480 this.$container.addClass(classes$12.coverOpen);
4481 this.coverIsOpen = true;
4482 } else {
4483 this.$container.removeClass(classes$12.coverOpen);
4484 this.coverIsOpen = false;
4485 }
4486
4487 // When pinching, a style attribute is added to the following elements with
4488 // the distance percentage. This callback will only get fired once at the
4489 // end of a pinch and ensures the cover will either open or close automatically
4490 // based on the conditional above.
4491 requestAnimationFrame(
4492 function() {
4493 this.$coverTopBackground.removeAttr('style');
4494 this.$coverBottomBackground.removeAttr('style');
4495 this.$coverTitle.removeAttr('style');
4496 }.bind(this)
4497 );
4498 }
4499};
4500
4501/*
4502
4503Side Scroller Extension
4504--------------------------------------------------------------------------------
4505Manages the desktop side scrolling behavior
4506
4507
4508Events
4509------------
4510
4511Name: side_scroller_init_start
4512Description: Fired at the start of the side scroller initialization
4513Payload: none
4514
4515Name: side_scroller_init_done
4516Description: Fired at the end of the side scroller initialization
4517Payload: none
4518
4519Name: side_scroller_destroy
4520Description: Fired when the side scroller is destroyed
4521Payload: none
4522
4523Name: side_scroller_position_top
4524Description: Fired when the position of the side scroller sticky image is switched to the top
4525Payload: none
4526
4527Name: side_scroller_position_fixed
4528Description: Fired when the position of the side scroller sticky image is switched to be fixed
4529Payload: none
4530
4531Name: side_scroller_position_bottom
4532Description: Fired when the position of the side scroller sticky image is switched to the bottom
4533Payload: none
4534
4535*/
4536
4537var selectors$15 = {
4538 coverContent: '.side-scroller__cover-content',
4539 slidesContainer: '.side-scroller__slides-container',
4540 coverBackground: '.side-scroller__cover-background',
4541 slides: '.side-scroller__slide'
4542};
4543
4544var classes$14 = {
4545 coverFixed: 'side-scroller--fixed',
4546 coverBottom: 'side-scroller--fixed-bottom'
4547};
4548
4549var sideScroller = {
4550 initSideScroller: function() {
4551 this.trigger('side_scroller_init_start');
4552
4553 this.$slides = $(selectors$15.slides, this.$container);
4554 this.$slidesContainer = $(selectors$15.slidesContainer, this.$container);
4555
4556 this.sideScroller = true;
4557 this.sideScrollerNamespace = '.sideScroller';
4558 this.coverPosition = 'top';
4559
4560 this.window().on(
4561 'scroll' + this.sideScrollerNamespace,
4562 this._onScroll.bind(this)
4563 );
4564 this.window().on(
4565 'resize' + this.sideScrollerNamespace,
4566 this._updateCoverPositionValues.bind(this)
4567 );
4568 this.window().on(
4569 'resize' + this.sideScrollerNamespace,
4570 this._onScroll.bind(this)
4571 );
4572
4573 this.document().on(
4574 'drawer_open_start' + this.sideScrollerNamespace,
4575 this._handleFixedSideImage.bind(this)
4576 );
4577 this.document().on(
4578 'drawer_close_done' + this.sideScrollerNamespace,
4579 this._resetFixedSideImage.bind(this)
4580 );
4581
4582 utils.promiseStylesheet().then(
4583 function() {
4584 this._updateCoverPositionValues();
4585 this._onScroll();
4586
4587 this.trigger('side_scroller_init_done');
4588 }.bind(this)
4589 );
4590 },
4591
4592 destroySideScroller: function() {
4593 this.sideScroller = false;
4594 this.window().off(this.sideScrollerNamespace);
4595 this.document().off(this.sideScrollerNamespace);
4596
4597 this.trigger('side_scroller_destroy');
4598 },
4599
4600 _handleFixedSideImage: function() {
4601 if (this.coverPosition !== 'fixed') return;
4602
4603 var $coverContent = $(selectors$15.coverContent, this.$container);
4604 var coverContentHeight = $coverContent.outerHeight();
4605 var scrollTop = $(window).scrollTop();
4606 var translate = scrollTop - this.featuresOffset.top;
4607
4608 $(selectors$15.coverBackground, this.$container).css({
4609 transform: 'translateY(' + translate + 'px)',
4610 position: 'absolute'
4611 });
4612
4613 $(selectors$15.coverContent, this.$container).css({
4614 transform:
4615 'translate(-50%, ' + (translate - coverContentHeight / 2) + 'px)',
4616 position: 'absolute'
4617 });
4618 },
4619
4620 _resetFixedSideImage: function() {
4621 if (this.coverPosition !== 'fixed') return;
4622
4623 $(selectors$15.coverBackground, this.$container)
4624 .add(selectors$15.coverContent, this.$container)
4625 .css({
4626 transform: '',
4627 position: ''
4628 });
4629 },
4630
4631 _onScroll: function() {
4632 var scrollTop = $(window).scrollTop();
4633
4634 // If the top of the window has gone past the top of the featured slider
4635 // and the window has not gone past the bottom of the featured slider,
4636 // we set the cover to fixed.
4637 // Debounce any style changes to the next from with requestAnimationFrame.
4638 if (
4639 scrollTop > this.featuresOffset.top &&
4640 scrollTop + this.windowHeight <=
4641 this.featuresOffset.top + this.featuresHeight
4642 ) {
4643 requestAnimationFrame(this._setCoverPosition.bind(this, 'fixed'));
4644 requestAnimationFrame(this._updateBackgroundColor.bind(this, scrollTop));
4645 } else if (
4646 scrollTop + this.windowHeight >
4647 this.featuresOffset.top + this.featuresHeight
4648 ) {
4649 requestAnimationFrame(this._setCoverPosition.bind(this, 'bottom'));
4650 } else {
4651 requestAnimationFrame(this._setCoverPosition.bind(this, 'top'));
4652 }
4653 },
4654
4655 _setCoverPosition: function(position) {
4656 if (this.coverPosition !== position) {
4657 switch (position) {
4658 case 'top':
4659 this.$container.removeClass(
4660 [classes$14.coverBottom, classes$14.coverFixed].join(' ')
4661 );
4662 this.trigger('side_scroller_position_top');
4663 break;
4664 case 'fixed':
4665 this.$container
4666 .removeClass(classes$14.coverBottom)
4667 .addClass(classes$14.coverFixed);
4668 this.trigger('side_scroller_position_fixed');
4669 break;
4670 case 'bottom':
4671 this.$container
4672 .removeClass(classes$14.coverFixed)
4673 .addClass(classes$14.coverBottom);
4674 this.trigger('side_scroller_position_bottom');
4675 break;
4676 }
4677 }
4678
4679 this.coverPosition = position;
4680 },
4681
4682 _updateBackgroundColor: function(scrollTop) {
4683 // The slide index is determined based on the current top position of the
4684 // window relative to the height of the slide.
4685 var currentSlideIndex = Math.floor(
4686 (scrollTop - this.featuresOffset.top + this.windowHeight / 2) /
4687 this.slideHeight
4688 );
4689
4690 this.currentBackgroundIndex = this.currentBackgroundIndex || 0;
4691
4692 if (currentSlideIndex !== this.currentBackgroundIndex) {
4693 this.currentBackgroundIndex = currentSlideIndex;
4694 this.$slidesContainer.css({
4695 backgroundColor: this.$slides
4696 .eq(currentSlideIndex)
4697 .data('background-color')
4698 });
4699 }
4700 },
4701
4702 _updateCoverPositionValues: function() {
4703 this.slideHeight = this.$slides.first().height();
4704 this.featuresOffset = this.$container.offset();
4705 this.featuresHeight = this.$container.height();
4706 this.windowHeight = $(window).innerHeight();
4707 this.windowWidth = $(window).innerWidth();
4708 }
4709};
4710
4711// Because the functionality is vastly different on mobile vs desktop, we
4712// require two different extensions and load them here into one section
4713sections.register('featured-slider', {
4714 onLoad: function() {
4715 this.coverIsOpen = false;
4716
4717 this.extend(revealSlider);
4718 this.extend(sideScroller);
4719
4720 this._toggleViewState();
4721
4722 $(window).on('resize', debounce(this._toggleViewState.bind(this)));
4723 },
4724
4725 onUnload: function() {
4726 this._destroyDesktopState();
4727 this._destroyMobileState();
4728 },
4729
4730 onDeselect: function() {
4731 if (this.mobileViewEnabled) {
4732 this.closeCover();
4733 }
4734 },
4735
4736 onBlockSelect: function(evt) {
4737 if (this.mobileViewEnabled) {
4738 this.openCover();
4739 this.slider.setSlide($(evt.target).index());
4740 } else {
4741 $('html, body').animate(
4742 {
4743 scrollTop: $(evt.target).offset().top
4744 },
4745 400
4746 );
4747 }
4748 },
4749
4750 onBlockDeselect: function() {
4751 if (this.mobileViewEnabled) {
4752 this.revealSlider.closeCover();
4753 }
4754 },
4755
4756 _toggleViewState: function() {
4757 var windowWidth = $(window).innerWidth();
4758 var enableMobileView;
4759 var enableDesktopView;
4760
4761 if (typeof this.mobileViewEnabled === 'undefined') {
4762 enableMobileView = windowWidth < theme.mediaQuerySmall;
4763 enableDesktopView = windowWidth >= theme.mediaQuerySmall;
4764 } else {
4765 enableMobileView =
4766 windowWidth < theme.mediaQuerySmall && !this.mobileViewEnabled;
4767 enableDesktopView =
4768 windowWidth >= theme.mediaQuerySmall && this.mobileViewEnabled;
4769 }
4770
4771 if (enableMobileView) {
4772 this.mobileViewEnabled = true;
4773 this._destroyDesktopState();
4774 this._enableMobileState();
4775 }
4776
4777 if (enableDesktopView) {
4778 this.mobileViewEnabled = false;
4779 this._destroyMobileState();
4780 this._enableDesktopState();
4781 }
4782 },
4783
4784 _enableDesktopState: function() {
4785 this.initSideScroller();
4786 },
4787
4788 _destroyDesktopState: function() {
4789 if (typeof this.sideScroller !== 'undefined') {
4790 this.destroySideScroller();
4791 }
4792 },
4793
4794 _enableMobileState: function() {
4795 this.initRevealSlider();
4796 },
4797
4798 _destroyMobileState: function() {
4799 if (this.revealSlider) {
4800 this.destroyRevealSlider();
4801 }
4802 }
4803});
4804
4805var classes$15 = {
4806 footerParallax: 'site-footer--parallax'
4807};
4808
4809var selectors$16 = {
4810 contentForLayout: '.content-for-layout',
4811 footerCover: '.site-footer__drawer-cover '
4812};
4813
4814sections.register('footer', {
4815 onLoad: function() {
4816 if (
4817 !this.container.hasAttribute('data-footer-parallax') ||
4818 ($('html').hasClass('is-ios') && Shopify.designMode)
4819 )
4820 return;
4821
4822 this.window().on('resize', this._setParallax.bind(this));
4823 this.document().on(
4824 'drawer_open_start',
4825 this._handleDrawerOpenState.bind(this)
4826 );
4827 this.document().on(
4828 'drawer_close_done',
4829 this._resetDrawerOpenState.bind(this)
4830 );
4831
4832 $(document.body).addClass(classes$15.footerParallax);
4833
4834 utils.promiseStylesheet().then(this._setParallax.bind(this));
4835 },
4836
4837 onUnload: function() {
4838 $(document.body).removeClass(classes$15.footerParallax);
4839 $(selectors$16.contentForLayout).css('margin-bottom', '');
4840 },
4841
4842 _setParallax: function() {
4843 this.height = this.$container.innerHeight();
4844 $(selectors$16.contentForLayout).css('margin-bottom', this.height);
4845 },
4846
4847 _handleDrawerOpenState: function() {
4848 if (document.body.scrollHeight - window.innerHeight === 0) {
4849 // If the page is smaller than the window
4850 this.$container.css(
4851 'margin-bottom',
4852 document.body.clientHeight - document.body.scrollHeight
4853 );
4854 } else {
4855 // If the page is longer than the window
4856 var distanceFromBottom =
4857 document.body.scrollHeight - window.innerHeight - window.scrollY;
4858 var adjustFooter = distanceFromBottom - this.height;
4859
4860 this.$container.css('margin-bottom', adjustFooter);
4861 $(selectors$16.footerCover)
4862 .css('height', distanceFromBottom)
4863 .show();
4864 }
4865 },
4866
4867 _resetDrawerOpenState: function() {
4868 this.$container.css('margin-bottom', '');
4869 $(selectors$16.footerCover).hide();
4870 }
4871});
4872
4873var selectors$17 = {
4874 skipLink: '.skip-link',
4875 navigation: '.navigation',
4876 navAnimateElements: '.navigation__entrance-animation',
4877 cartBubble: '.site-header__cart-bubble',
4878 siteHeader: '.site-header',
4879 siteHeaderWrapper: '.site-header__wrapper',
4880 siteHeaderLogo: '.site-header__logo',
4881 siteHeaderCart: '.site-header__cart',
4882 siteNavigation: '[data-site-navigation]',
4883 navigationButton: '[data-navigation-button]',
4884 navigationExpandSublinks: '.navigation__expand-sublinks',
4885 navigationExpandChildSublinks: '.navigation__expand-sublinks--third-level',
4886 navigationSublinksContainer: '.navigation__sublinks-container',
4887 navigationSublink: '.navigation__sublink',
4888 navigationSublinks: '.navigation__sublinks',
4889 navigationChildSublinks: '.navigation__sublinks--third-level',
4890 navigationHasSublinks: '.navigation__has-sublinks',
4891 announcementBar: '.announcement-bar',
4892 announcementBarClose: '.announcement-bar__close',
4893 contentForLayout: '.content-for-layout'
4894};
4895
4896var classes$16 = {
4897 themeEditor: 'theme-editor',
4898 headerFixed: 'site-header--fixed',
4899 headerHomepage: 'site-header--homepage',
4900 headerTransparent: 'site-header--transparent',
4901 cartBubbleVisible: 'site-header__cart-bubble--visible',
4902 bubblePulse: 'bubble-pulse',
4903 drawerActive: 'drawer--active',
4904 navigationExpandChildSublinks: 'navigation__expand-sublinks--third-level',
4905 navigationOpen: 'navigation-open',
4906 navigationOpenEditor: 'navigation-open--editor',
4907 navigationTransitioning: 'navigation--is-transitioning',
4908 navigationHasSublinksCollapsed: 'navigation__has-sublinks--collapsed',
4909 navigationEntranceActive: 'navigation__entrance-animation--active',
4910 siteHeaderWrapperTransitioning: 'site-header__wrapper--transitioning',
4911 announcementBarVisible: 'announcement-bar--visible'
4912};
4913
4914var sectionsWithTransparentHeader = ['slideshow-section', 'gallery', 'hero'];
4915var scrollHandler = true;
4916var blockEditorEvents = false;
4917var navigationManuallyToggled = false;
4918
4919sections.register('header-section', {
4920 /*--------------------------------------------------------------------------*/
4921 /* Editor Events */
4922 /*--------------------------------------------------------------------------*/
4923 onLoad: function() {
4924 this.$container = $(this.container);
4925 this.$siteHeader = $(selectors$17.siteHeader);
4926 this.$siteHeaderWrapper = $(selectors$17.siteHeaderWrapper);
4927
4928 this.scrollTop = 0;
4929 this.isFixed = false;
4930 this.headerHeight = 0;
4931 this.promiseChain = $.Deferred().resolve();
4932 this._updateHeaderTransparency();
4933
4934 // Initially disable tabbing through site navigation
4935 utils.disableTabbingOfChildren(selectors$17.navigation);
4936
4937 $(selectors$17.navigationButton)
4938 .one('click', this._toggleNavigationDisplay.bind(this))
4939 .one('click', this._setSublinksMaxHeight.bind(this));
4940
4941 $(selectors$17.navigationExpandSublinks, this.$container).on(
4942 'click',
4943 this._toggleNavigationSublinks.bind(this)
4944 );
4945
4946 $(selectors$17.announcementBarClose, this.$container).on(
4947 'click',
4948 this._closeAnnouncementBar.bind(this)
4949 );
4950
4951 // Disable the sticky and/or transparent header in iOS editor
4952 if ($('html').hasClass('is-ios') && Shopify.designMode) {
4953 this.$siteHeader.removeClass(classes$16.headerHomepage);
4954 this.$siteHeader.removeClass(classes$16.headerTransparent);
4955 } else {
4956 // Don't call _toggleHeaderPosition on scroll in the iOS editor
4957 $(document).on(
4958 'scroll' + this.namespace,
4959 throttle(this._toggleHeaderPosition.bind(this), 200)
4960 );
4961 }
4962
4963 $(window).on(
4964 'resize' + this.namespace,
4965 this._adjustNavigationPadding.bind(this)
4966 );
4967
4968 $(document).on(
4969 'shopify:section:reorder shopify:section:load',
4970 this._updateHeaderTransparency.bind(this)
4971 );
4972
4973 $(document).on(
4974 'shopify:section:unload',
4975 function() {
4976 setTimeout(this._updateHeaderTransparency.bind(this));
4977 }.bind(this)
4978 );
4979
4980 cart.on('change', this._toggleCartBubble.bind(this));
4981
4982 utils.promiseStylesheet().then(
4983 function() {
4984 this._setupAnnouncementBar();
4985 }.bind(this)
4986 );
4987 },
4988
4989 onSelect: function() {
4990 this._adjustNavigationPadding();
4991
4992 // If you close the navigation to view and edit the transparent header
4993 // settings, then you don't want the navigation to open when it reloads
4994 // with your changes. Skip opening the navigation if the user has clicked
4995 // on the 'X' of the navigation.
4996 if (navigationManuallyToggled) return;
4997
4998 // Block the 'onDeselect' if it is fired within 100ms
4999 blockEditorEvents = true;
5000
5001 this.showNavigation();
5002 },
5003
5004 onDeselect: function() {
5005 // Make sure that we're not blocking editor events by default
5006 blockEditorEvents = false;
5007
5008 // When a section is reloaded by the editor it fires the 'onDeselect',
5009 // 'onUnload', 'onLoad', and then 'onSelect' events. We don't want to hide
5010 // the navigation when the section reloads from an edit. If we receive a
5011 // `onSelect` event within 100ms of the 'onDeselect' event, then it is
5012 // canceled.
5013 setTimeout(
5014 function() {
5015 if (blockEditorEvents) return;
5016
5017 navigationManuallyToggled = false;
5018 blockEditorEvents = false;
5019
5020 this.hideNavigation();
5021 }.bind(this),
5022 100
5023 );
5024 },
5025
5026 /*--------------------------------------------------------------------------*/
5027 /* Header */
5028 /*--------------------------------------------------------------------------*/
5029 _setupAnnouncementBar: function() {
5030 var $announcementBar = $(selectors$17.announcementBar, this.$container);
5031
5032 if (!$announcementBar.length) return;
5033
5034 if (
5035 utils.isSessionStorageSupported() &&
5036 sessionStorage.getItem('announcement-bar-hidden')
5037 )
5038 return;
5039
5040 requestAnimationFrame(
5041 function() {
5042 $announcementBar.addClass(classes$16.announcementBarVisible);
5043 this.$siteHeaderWrapper.css(
5044 'margin-top',
5045 '-' + $announcementBar.outerHeight() + 'px'
5046 );
5047
5048 requestAnimationFrame(
5049 function() {
5050 this.$siteHeaderWrapper
5051 .addClass(classes$16.siteHeaderWrapperTransitioning)
5052 .css('margin-top', '');
5053 }.bind(this)
5054 );
5055 }.bind(this)
5056 );
5057 },
5058
5059 _closeAnnouncementBar: function(evt) {
5060 evt.preventDefault();
5061
5062 if (Shopify.designMode) return;
5063
5064 var $announcementBar = $(selectors$17.announcementBar);
5065
5066 this.$siteHeaderWrapper.css(
5067 'margin-top',
5068 '-' + $announcementBar.outerHeight() + 'px'
5069 );
5070
5071 $(selectors$17.announcementBarClose).attr('aria-expanded', false);
5072
5073 if (utils.isSessionStorageSupported()) {
5074 sessionStorage.setItem('announcement-bar-hidden', true);
5075 }
5076
5077 utils.promiseTransitionEnd(this.$siteHeaderWrapper).then(
5078 function() {
5079 $announcementBar.remove();
5080
5081 this.$siteHeaderWrapper
5082 .removeClass(classes$16.siteHeaderWrapperTransitioning)
5083 .css('margin-top', '');
5084 }.bind(this)
5085 );
5086 },
5087
5088 _updateHeaderTransparency: function() {
5089 if ($('html').hasClass('is-ios') && Shopify.designMode) return;
5090
5091 this.$siteHeader.removeClass(classes$16.headerTransparent);
5092
5093 // Only continue if we're on the homepage and transparent header setting is enabled.
5094 if (!this.$siteHeader.data('transparent-header')) return;
5095
5096 var $firstSection = $(selectors$17.contentForLayout)
5097 .find('[data-section-type]')
5098 .first();
5099
5100 // Only continue if first section exists and its type allows for a transparent header
5101 if (
5102 !$firstSection.length ||
5103 sectionsWithTransparentHeader.indexOf(
5104 $firstSection.data('section-type')
5105 ) === -1
5106 )
5107 return;
5108
5109 this.$siteHeader.addClass(classes$16.headerTransparent);
5110 },
5111
5112 _toggleHeaderPosition: function() {
5113 // Check if toggle is disabled by other settings
5114 if (!scrollHandler || this.headerAnimating) return;
5115
5116 var scrollTop = $(document).scrollTop();
5117
5118 this.headerHeight = this.headerHeight || this.$container.outerHeight();
5119
5120 // Check which header transformation needs to happen. Also check if a toggle
5121 // even needs to happen, i.e. if you're scrolling up and the header is
5122 // already displayed, why call slideOut again?
5123 // Debounce any style changes to the next frame using requestAnimationFrame
5124 if (scrollTop <= 0) {
5125 requestAnimationFrame(this._promiseHeaderReset.bind(this));
5126 } else if (
5127 scrollTop < this.scrollTop &&
5128 scrollTop > 0 &&
5129 !this.isFixed &&
5130 !this.isAnimating
5131 ) {
5132 requestAnimationFrame(this._promiseHeaderSlideIn.bind(this));
5133 } else if (
5134 scrollTop > this.scrollTop &&
5135 scrollTop > this.headerHeight &&
5136 this.isFixed &&
5137 !this.isAnimating
5138 ) {
5139 requestAnimationFrame(this._promiseHeaderSlideOut.bind(this));
5140 }
5141
5142 this.scrollTop = scrollTop;
5143
5144 // The scroll event is fired randomly and is not guaranteed to fire when
5145 // scrollTop === 0. Double check after a short period to see if we need to
5146 // reset the header.
5147 this._doubleCheckPosition();
5148 },
5149
5150 _doubleCheckPosition: function() {
5151 if (typeof this.doubleCheckDebounced === 'undefined') {
5152 this.doubleCheckDebounced = debounce(
5153 function() {
5154 this.scrollTop = $(document).scrollTop();
5155 if (this.scrollTop <= 0) {
5156 this._promiseHeaderReset();
5157 }
5158 }.bind(this),
5159 500
5160 );
5161 }
5162
5163 this.doubleCheckDebounced();
5164 },
5165
5166 _promiseHeaderReset: function() {
5167 this.promiseChain = this.promiseChain.then(
5168 function() {
5169 this.$container.removeClass(classes$16.headerFixed);
5170 $('body').css('padding-top', '');
5171 }.bind(this)
5172 );
5173 },
5174
5175 _promiseHeaderSlideIn: function() {
5176 this.isFixed = true;
5177 this.headerAnimating = true;
5178 this.$siteHeaderWrapper.css('transform', 'translateY(-100%)');
5179
5180 requestAnimationFrame(
5181 function() {
5182 this.$container.addClass(classes$16.headerFixed);
5183
5184 if (!this.$container.hasClass(classes$16.headerHomepage)) {
5185 $('body').css('padding-top', this.headerHeight);
5186 }
5187
5188 requestAnimationFrame(
5189 function() {
5190 this.$siteHeaderWrapper.css({
5191 transform: 'translateY(0%)',
5192 transition: 'transform 0.25s ease-out'
5193 });
5194 }.bind(this)
5195 );
5196 }.bind(this)
5197 );
5198
5199 return utils.promiseTransitionEnd(this.$siteHeaderWrapper).then(
5200 function() {
5201 this.headerAnimating = false;
5202 this.$siteHeaderWrapper.attr('style', '');
5203 }.bind(this)
5204 );
5205 },
5206
5207 _promiseHeaderSlideOut: function() {
5208 this.isFixed = false;
5209 this.headerAnimating = true;
5210
5211 this.$siteHeaderWrapper.css({
5212 transform: 'translateY(-100%)',
5213 transition: 'transform 0.25s ease-out'
5214 });
5215
5216 return utils.promiseTransitionEnd(this.$siteHeaderWrapper).then(
5217 function() {
5218 this.headerAnimating = false;
5219 this.$siteHeaderWrapper.attr('style', '');
5220 $('body').css('padding-top', '');
5221 this.$container.removeClass(classes$16.headerFixed);
5222 }.bind(this)
5223 );
5224 },
5225
5226 _toggleCartBubble: function(evt, state) {
5227 var $bubble = $(selectors$17.cartBubble, this.$container);
5228
5229 $bubble.toggleClass(classes$16.cartBubbleVisible, state.items.length !== 0);
5230
5231 if (!$('body').hasClass(classes$16.drawerActive)) {
5232 $bubble.addClass(classes$16.bubblePulse);
5233
5234 utils.promiseAnimationEnd($bubble).then(function() {
5235 $bubble.removeClass(classes$16.bubblePulse);
5236 });
5237 }
5238 },
5239
5240 /*--------------------------------------------------------------------------*/
5241 /* Navigation */
5242 /*--------------------------------------------------------------------------*/
5243 showNavigation: function() {
5244 var isOpen = $(document.body).hasClass(classes$16.navigationOpen);
5245 this.siteNavigation = this.container.querySelector(
5246 selectors$17.siteNavigation
5247 );
5248 var navigationButton = this.siteNavigation.querySelector(
5249 selectors$17.navigationButton
5250 );
5251
5252 if (!isOpen) {
5253 scrollHandler = false;
5254
5255 this._adjustNavigationPadding();
5256 a11y.trapFocus(this.siteNavigation, {
5257 elementToFocus: navigationButton
5258 });
5259
5260 utils.enableTabbingOfChildren(selectors$17.navigation);
5261 $(selectors$17.navigation).attr('aria-hidden', false);
5262
5263 this.promiseChain = this.promiseChain
5264 .then(this._promiseNavTransition.bind(this, true))
5265 .then(
5266 this._promiseNavItemsTransition.bind(
5267 this,
5268 $(selectors$17.navAnimateElements, this.$container).toArray(),
5269 true
5270 )
5271 )
5272 .then(
5273 function() {
5274 $(document).on(
5275 'keyup' + this.namespace,
5276 this._closeOnEscape.bind(this)
5277 );
5278 $(selectors$17.navigationButton)
5279 .attr('aria-expanded', true)
5280 .off()
5281 .one('click', this._toggleNavigationDisplay.bind(this));
5282 }.bind(this)
5283 );
5284 }
5285
5286 return this.promiseChain.then(function() {
5287 // This class is only applied in the Theme Editor and makes sure that
5288 // the navigation links show when the section reloads from settings
5289 // changes.
5290 if (Shopify.designMode) {
5291 $(document.body).addClass(classes$16.navigationOpenEditor);
5292 }
5293 });
5294 },
5295
5296 hideNavigation: function() {
5297 var isOpen = $(document.body).hasClass(classes$16.navigationOpen);
5298
5299 if (isOpen) {
5300 var $navElements = $(selectors$17.navAnimateElements, this.$container);
5301
5302 a11y.removeTrapFocus();
5303
5304 utils.disableTabbingOfChildren(selectors$17.navigation);
5305 $(selectors$17.navigation).attr('aria-hidden', true);
5306
5307 if (Shopify.designMode) {
5308 $(document.body).removeClass(classes$16.navigationOpenEditor);
5309 $navElements.addClass(classes$16.navigationEntranceActive);
5310 }
5311
5312 this.promiseChain = this.promiseChain
5313 .then(
5314 this._promiseNavItemsTransition.bind(
5315 this,
5316 $navElements.toArray().reverse(),
5317 false
5318 )
5319 )
5320 .then(this._promiseNavTransition.bind(this, false))
5321 .then(
5322 function() {
5323 scrollHandler = true;
5324 $(document).off('keyup' + this.namespace);
5325 $(selectors$17.navigationButton)
5326 .off()
5327 .one('click', this._toggleNavigationDisplay.bind(this))
5328 .attr('aria-expanded', false)
5329 .focus();
5330 }.bind(this)
5331 );
5332 }
5333
5334 return this.promiseChain;
5335 },
5336
5337 _toggleNavigationDisplay: function() {
5338 navigationManuallyToggled = true;
5339
5340 if ($(document.body).hasClass(classes$16.navigationOpen)) {
5341 this.hideNavigation();
5342 } else {
5343 this.showNavigation();
5344 }
5345 },
5346
5347 _adjustNavigationPadding: function() {
5348 this.headerHeight = this.$siteHeader.height();
5349 $(selectors$17.navigation, this.$container).css({
5350 top: this.headerHeight + 'px',
5351 'min-height': 'calc(100vh - ' + this.headerHeight + 'px)',
5352 'max-height': 'calc(100vh - ' + this.headerHeight + 'px)'
5353 });
5354 },
5355
5356 _promiseNavTransition: function(active) {
5357 $(document.body)
5358 .addClass(classes$16.navigationTransitioning)
5359 .toggleClass(classes$16.navigationOpen, active);
5360
5361 $(window).scrollTop(this.scrollTop);
5362
5363 return utils
5364 .promiseTransitionEnd($(selectors$17.navigation, this.$container))
5365 .then(function() {
5366 $(document.body).removeClass(classes$16.navigationTransitioning);
5367 });
5368 },
5369
5370 _promiseNavItemsTransition: function(elements, active) {
5371 return utils.mapPromiseSeries(elements, function(index, element) {
5372 var $element = $(element);
5373
5374 $element.toggleClass(classes$16.navigationEntranceActive, active);
5375
5376 return utils.promiseTransitionEnd($element);
5377 });
5378 },
5379
5380 _closeOnEscape: function(evt) {
5381 if (evt.keyCode === 27) {
5382 this.hideNavigation();
5383 }
5384 },
5385
5386 _toggleNavigationSublinks: function(evt) {
5387 var $anchor = $(evt.currentTarget);
5388 var $parent = $anchor.parent();
5389 var isExpanded = $anchor.attr('aria-expanded') === 'true';
5390 var $childSublinks = $parent.find(selectors$17.navigationExpandChildSublinks);
5391
5392 $parent.toggleClass(classes$16.navigationHasSublinksCollapsed);
5393 $parent
5394 .siblings(selectors$17.navigationHasSublinks)
5395 .addClass(classes$16.navigationHasSublinksCollapsed)
5396 .children(selectors$17.navigationExpandSublinks)
5397 .attr('aria-expanded', false);
5398
5399 this._setMaxHeight($parent.siblings(selectors$17.navigationHasSublinks), '');
5400
5401 if ($childSublinks.length) {
5402 this._updateSublinkMaxHeight($anchor);
5403 }
5404
5405 $anchor.attr('aria-expanded', !isExpanded);
5406 this._toggleSubNavigationElementHeight($anchor);
5407 },
5408
5409 _updateSublinkMaxHeight: function($anchor) {
5410 var isChildSublink = $anchor.hasClass(
5411 classes$16.navigationExpandChildSublinks
5412 );
5413
5414 if (!isChildSublink) {
5415 return;
5416 }
5417
5418 var $sublinkContainer = $anchor.closest(
5419 selectors$17.navigationSublinksContainer
5420 );
5421 var $childSublinks = $sublinkContainer.find(
5422 selectors$17.navigationHasSublinks
5423 );
5424
5425 var maxHeight = $sublinkContainer.data('max-height');
5426
5427 $childSublinks.each(function() {
5428 var $el = $(this);
5429
5430 if ($el.hasClass(classes$16.navigationHasSublinksCollapsed)) {
5431 return;
5432 }
5433
5434 var $childSublink = $el.find(selectors$17.navigationChildSublinks);
5435 var $parent = $childSublink.parent();
5436
5437 maxHeight += $parent.data('max-height');
5438 });
5439
5440 $sublinkContainer.css('max-height', maxHeight);
5441 },
5442
5443 _setSublinksMaxHeight: function() {
5444 $(selectors$17.navigationSublinks, this.$container).each(function() {
5445 var $el = $(this);
5446 var $parent = $el.parent();
5447 var sublinkHeight = $el.outerHeight();
5448
5449 $parent
5450 .data('max-height', sublinkHeight)
5451 .css('max-height', sublinkHeight);
5452 });
5453 },
5454
5455 _toggleSubNavigationElementHeight: function($anchor) {
5456 var $currentListItem = $anchor.parent('li');
5457 if ($currentListItem.hasClass(classes$16.navigationHasSublinksCollapsed)) {
5458 this._setMaxHeight($currentListItem, '');
5459 } else {
5460 if ($currentListItem.hasClass(selectors$17.navigationSublink)) {
5461 this._updateSublinkMaxHeight($currentListItem);
5462 return;
5463 }
5464 this._setMaxHeight($currentListItem, 'none');
5465 }
5466 },
5467
5468 _setMaxHeight: function(element, maxHeight) {
5469 element.css('max-height', maxHeight);
5470 }
5471});
5472
5473/*
5474
5475List Collections Template Section
5476--------------------------------------------------------------------------------
5477Creates a staggered card grid for the list collections template.
5478
5479*/
5480
5481// Extensions
5482// Libs
5483sections.register('list-collections-template', {
5484 onLoad: function() {
5485 this.extend(cardManager);
5486 }
5487});
5488
5489var promiseGoogleMapsAPI;
5490
5491var errors = {
5492 zeroResults: theme.strings.map && theme.strings.map.zeroResults,
5493 overQueryLimit: theme.strings.map && theme.strings.map.overQueryLimit,
5494 authError: theme.strings.map && theme.strings.map.authError,
5495 requestDenied: theme.strings.map && theme.strings.map.addressError,
5496 geocodeUnknownError: theme.strings.map && theme.strings.map.addressError,
5497 missingKey: theme.strings.map && theme.strings.map.addressError,
5498 apiLoadError: theme.strings.map && theme.strings.map.authError
5499};
5500
5501function errorMessage(status) {
5502 var error;
5503
5504 switch (status) {
5505 case 'ZERO_RESULTS':
5506 error = errors.zeroResults;
5507 break;
5508 case 'OVER_QUERY_LIMIT':
5509 error = errors.overQueryLimit;
5510 break;
5511 case 'REQUEST_DENIED':
5512 error = errors.requestDenied;
5513 break;
5514 case 'UNKNOWN_ERROR':
5515 error = errors.geocodeUnknownError;
5516 break;
5517 default:
5518 error = errors.geocodeUnknownError;
5519 }
5520
5521 return error;
5522}
5523
5524var googleMaps = {
5525 promiseAPI: function(key) {
5526 key = key || '';
5527
5528 if (typeof promiseGoogleMapsAPI === 'undefined') {
5529 promiseGoogleMapsAPI = $.getScript(
5530 'https://maps.googleapis.com/maps/api/js?key=' + key
5531 );
5532 }
5533
5534 return promiseGoogleMapsAPI.then(function() {
5535 return $.Deferred(function(defer) {
5536 if (!window.google || !window.google.maps) {
5537 defer.reject(errors.apiLoadError);
5538 }
5539
5540 defer.resolve(window.google);
5541 });
5542 });
5543 },
5544
5545 promiseGeocode: function(address) {
5546 return this.promiseAPI().then(function(google) {
5547 return $.Deferred(function(defer) {
5548 var geocoder;
5549
5550 // Global function called by Google on auth errors.
5551 window.gm_authFailure = function() {
5552 // eslint-disable-line camelcase
5553 defer.reject(errors.authError);
5554 };
5555
5556 geocoder = new google.maps.Geocoder();
5557 geocoder.geocode({ address: address }, function(results, status) {
5558 if (status !== google.maps.GeocoderStatus.OK) {
5559 defer.reject(errorMessage(status));
5560 }
5561 defer.resolve(results);
5562 });
5563 });
5564 });
5565 },
5566
5567 promiseMap: function(config) {
5568 return $.when(
5569 this.promiseAPI(config.key),
5570 this.promiseGeocode(config.address)
5571 ).then(function(google, results) {
5572 return $.Deferred(function(defer) {
5573 var map;
5574 var center = results[0].geometry.location;
5575 var mapOptions = $.extend(
5576 {},
5577 {
5578 zoom: 14,
5579 center: center,
5580 disableDefaultUI: true
5581 },
5582 config
5583 );
5584
5585 // Global function called by Google on auth errors.
5586 // Show an auto error message on all map instances.
5587 window.gm_authFailure = function() {
5588 // eslint-disable-line camelcase
5589 defer.reject(errors.authError);
5590 };
5591
5592 map = new google.maps.Map(config.container, mapOptions);
5593 map.centerMarker = new google.maps.Marker({
5594 map: map,
5595 position: map.getCenter()
5596 });
5597
5598 google.maps.event.addDomListener(
5599 window,
5600 'resize',
5601 debounce(function() {
5602 google.maps.event.trigger(map, 'resize');
5603 map.setCenter(center);
5604 }, 250)
5605 );
5606
5607 return defer.resolve(map);
5608 });
5609 });
5610 }
5611};
5612
5613/*
5614
5615Map Section
5616--------------------------------------------------------------------------------
5617Loads a static map in the background of the section
5618
5619*/
5620
5621// Libs
5622var selectors$18 = {
5623 map: '.map-section__container',
5624 mapError: '.map-section__error-message'
5625};
5626
5627var classes$17 = {
5628 mapError: 'map-section--load-error'
5629};
5630
5631sections.register('map', {
5632 onLoad: function() {
5633 this.$container = $(this.container);
5634 this.$map = $(selectors$18.map, this.$container);
5635 this.key = this.$map.data('api-key');
5636
5637 if (typeof this.key !== 'string' || this.key === '') return;
5638
5639 var config = {
5640 key: this.key,
5641 container: this.$map[0],
5642 address: this.$map.data('address-setting')
5643 };
5644
5645 utils
5646 .promiseStylesheet()
5647 .then(function() {
5648 return googleMaps.promiseMap(config);
5649 })
5650 .then(
5651 function(map) {
5652 this.map = map;
5653 }.bind(this)
5654 )
5655 .catch(
5656 function(error) {
5657 this.$container.addClass(classes$17.mapError);
5658
5659 // Only show error in the theme editor
5660 if (Shopify.designMode) {
5661 $(selectors$18.mapError, this.$container)
5662 .html(error)
5663 .css('display', 'inline-block');
5664 }
5665 }.bind(this)
5666 );
5667 }
5668});
5669
5670/*
5671
5672Newsletter Section
5673--------------------------------------------------------------------------------
5674Overrides default HTML5 error checking for the newsletter section.
5675
5676*/
5677
5678// Libs
5679var selectors$19 = {
5680 emailField: '#EmailField',
5681 inputGroup: '.input-group--underline',
5682 submitButton: '.btn--newsletter__submit',
5683 newsletterMessage: '.newsletter__message'
5684};
5685
5686var errors$1 = {
5687 blankError: theme.strings.newsletter && theme.strings.newsletter.blankError,
5688 invalidError:
5689 theme.strings.newsletter && theme.strings.newsletter.invalidError
5690};
5691
5692sections.register('newsletter', {
5693 onLoad: function() {
5694 if ($('html').hasClass('is-ios') && Shopify.designMode) {
5695 $(selectors$19.inputGroup, this.$container).addClass('has-animated');
5696 }
5697
5698 this.$container.on(
5699 'click',
5700 selectors$19.submitButton,
5701 this._checkEmail.bind(this)
5702 );
5703 },
5704
5705 _checkEmail: function() {
5706 var emailFieldID =
5707 selectors$19.emailField + this.$container.data('section-id');
5708 var $emailField = $(emailFieldID, this.$container);
5709 var $newsletterMessage = $(selectors$19.newsletterMessage, this.$container);
5710 var errorMessage;
5711 var emailValue = $emailField[0].value;
5712 var emailRegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
5713
5714 if (emailValue === '') {
5715 errorMessage = errors$1.blankError;
5716 } else if (!emailRegExp.test(emailValue)) {
5717 errorMessage = errors$1.invalidError;
5718 }
5719
5720 if (typeof errorMessage !== 'undefined') {
5721 $newsletterMessage.html('<p class="errors">' + errorMessage + '</p>');
5722 return false;
5723 } else {
5724 $newsletterMessage.html('');
5725 }
5726 }
5727});
5728
5729/*
5730
5731Product Recommendations Section
5732--------------------------------------------------------------------------------
5733Creates a staggered card grid for the product recommendations section.
5734
5735*/
5736
5737// Libs
5738var selectors$20 = {
5739 productRecommendationSlider: '[data-recommendations-slider]',
5740 productRecommendationSlides: '[data-recommendations-slide]',
5741 indicatorsWrapper: '[data-recommendations-indicators]',
5742 indicator: '[data-recommendations-indicator]',
5743 productRecommendationIndicatorIndex: function(index) {
5744 return '[data-product-recommendation-indicator-index="' + index + '"]';
5745 }
5746};
5747
5748var classes$18 = {
5749 indicatorActive: 'product-recommendations__slide-indicator--active'
5750};
5751
5752var mobileMediaQuery = '(max-width: 749px)';
5753
5754sections.register('product-recommendations', {
5755 onLoad: function() {
5756 this.didShowRecommendations = false;
5757
5758 var baseUrl = this.$container.data('baseUrl');
5759 var productId = this.$container.data('productId');
5760 var recommendationsSectionUrl =
5761 baseUrl +
5762 '?section_id=product-recommendations&product_id=' +
5763 productId +
5764 '&limit=4';
5765
5766 utils
5767 .promiseStylesheet()
5768 .then(function() {
5769 return $.get(recommendationsSectionUrl);
5770 })
5771 .then(
5772 function(section) {
5773 var recommendationsMarkup = $(section).html();
5774 if (recommendationsMarkup.trim() !== '') {
5775 this.$container.html(recommendationsMarkup);
5776 this._initProductRecommendations();
5777
5778 this.didShowRecommendations = true;
5779 }
5780 }.bind(this)
5781 );
5782 },
5783
5784 _initProductRecommendations: function() {
5785 this.eventHandlers = {};
5786 this.mql = window.matchMedia(mobileMediaQuery);
5787 this._setupEventHandlers();
5788 this._mediaQueryHandler();
5789 },
5790
5791 _setupEventHandlers: function() {
5792 this.eventHandlers._mediaQueryHandler = this._mediaQueryHandler.bind(this);
5793 this.eventHandlers._updateIndicatorActiveState = this._updateIndicatorActiveState.bind(
5794 this
5795 );
5796
5797 this.mql.addListener(this.eventHandlers._mediaQueryHandler);
5798 },
5799
5800 _mediaQueryHandler: function() {
5801 if (this.mql.matches) {
5802 this._initializeMobileSlider();
5803 } else {
5804 this._destroyMobileSlider();
5805 }
5806 },
5807
5808 _initializeMobileSlider: function() {
5809 this.$slider =
5810 this.$slider ||
5811 this.$container.find(selectors$20.productRecommendationSlider);
5812 this.$slides =
5813 this.$slides || this.$slider.find(selectors$20.productRecommendationSlides);
5814
5815 // Don't init the slider if we have less than 2 slides
5816 if (this.$slides.length < 2) {
5817 return;
5818 }
5819
5820 this.$indicatorsWrapper =
5821 this.$indicatorsWrapper ||
5822 this.$container.find(selectors$20.indicatorsWrapper);
5823
5824 this.$indicators =
5825 this.$indicators || this.$indicatorsWrapper.find(selectors$20.indicator);
5826
5827 this.$slider.on(
5828 'scroll',
5829 { passive: true },
5830 this.eventHandlers._updateIndicatorActiveState
5831 );
5832
5833 this._updateIndicatorActiveState();
5834 },
5835
5836 _destroyMobileSlider: function() {
5837 if (!this.$slider) {
5838 return;
5839 }
5840
5841 this.$slider.off('scroll');
5842 },
5843
5844 _updateIndicatorActiveState: function() {
5845 this.$indicators.removeClass(classes$18.indicatorActive);
5846
5847 var scrollLeft = this.$slider.scrollLeft();
5848 var numberOfSlides = this.$slides.length;
5849 var slideWidth = this.$slider[0].scrollWidth / numberOfSlides;
5850 var activeIndicatorIndex = Math.round(scrollLeft / slideWidth) + 1;
5851
5852 var $indicator = this.$container.find(
5853 selectors$20.productRecommendationIndicatorIndex(activeIndicatorIndex)
5854 );
5855
5856 $indicator.addClass(classes$18.indicatorActive);
5857 },
5858
5859 onUnload: function() {
5860 if (!this.didShowRecommendations) {
5861 return;
5862 }
5863
5864 this.mql.removeListener(this.eventHandlers._mediaQueryHandler);
5865 this._destroyMobileSlider();
5866 }
5867});
5868
5869/*
5870
5871Product Slideshow Extension
5872--------------------------------------------------------------------------------
5873Adds the product slideshow functionality to a section
5874
5875Events
5876---------
5877
5878Name: product_slideshow_show
5879Description: Fired when the slideshow is opened
5880Payload: none
5881
5882Name: product_slideshow_hide
5883Description: Fired when the slideshow is closed
5884Payload: none
5885
5886Name: product_slideshow_set_slide
5887Description: Fired when the slide is being set
5888Payload: { number } Index of the slide
5889
5890Name: product_slideshow_next
5891Description: Fired when the next button is clicked
5892Payload: none
5893
5894Name: product_slideshow_previous
5895Description: Fired when the previous button is clicked
5896Payload: none
5897
5898*/
5899
5900var classes$20 = {
5901 lazypreload: 'lazypreload',
5902 showProductSlideshow: 'product-slideshow--show',
5903 productSlideshowSelectActive: 'product-slideshow__slide-select--active'
5904};
5905
5906var selectors$22 = {
5907 slider: '.slider',
5908 productSlideshow: '.product-slideshow',
5909 productSlideshowOpen: '.product-slideshow__open',
5910 productSlideshowSlide: '[data-product-slideshow-slide]',
5911 productSlideshowImage: '[data-product-slideshow-image]',
5912 productSlideshowNext: '[data-product-slideshow-next]',
5913 productSlideshowSelect: '[data-product-slideshow-select]',
5914 productSlideshowPrevious: '[data-product-slideshow-previous]',
5915 productSlideshowClose: '[data-product-slideshow-close]'
5916};
5917
5918var productSlideshow = {
5919 init: function() {
5920 this.$productSlideshow = $(selectors$22.productSlideshow, this.$container);
5921
5922 // If there are not enough images on the page we won't have a slideshow, so
5923 // return.
5924 if (this.$productSlideshow.length === 0) return;
5925
5926 $(selectors$22.productSlideshowOpen).on(
5927 'click',
5928 this._onImageClick.bind(this)
5929 );
5930
5931 // The product slideshow is shipped from the server inside the product template
5932 // section HTML. For styling reasons, the slideshow HTML needs to live as
5933 // the last child of body.
5934 this.$productSlideshow.appendTo(document.body);
5935
5936 // Check how many slides we have for the slideshow. If we only have one, then
5937 // we don't need any slide controls, so no need to initialize the Slider() lib
5938 this.slideCount = $(
5939 selectors$22.productSlideshowSlide,
5940 this.$productSlideshow
5941 ).length;
5942 if (this.slideCount > 1) {
5943 this.slider = new Slider($(selectors$22.slider, this.$productSlideshow));
5944 }
5945 },
5946
5947 showSlideshow: function() {
5948 this.trigger('product_slideshow_show');
5949
5950 // Cache the image that was clicked so that when we close the slideshow, we
5951 // can return the focus to it.
5952 this.$slideshowOpenFocus = $(document.activeElement);
5953
5954 // Add the lazysizes preload class so that we start preloading all slideshow
5955 // images
5956 $(selectors$22.productSlideshowImage, this.$productSlideshow).addClass(
5957 classes$20.lazypreload
5958 );
5959
5960 // Get the header section so that we can turn off the scroll handling so that
5961 // it doesn't disappear when we open the slideshow.
5962 return sections.getInstances('header-section').then(
5963 function(instances) {
5964 instances[0].scrollHandler = false;
5965
5966 this._assignEventHandlers();
5967
5968 a11y$1.trapFocus({
5969 $container: this.$productSlideshow,
5970 namespace: 'product-slideshow'
5971 });
5972
5973 this.$productSlideshow
5974 .addClass(classes$20.showProductSlideshow)
5975 .attr('aria-hidden', false);
5976
5977 utils.disableScrollBody();
5978 utils.enableTabbingOfChildren(this.$productSlideshow);
5979 }.bind(this)
5980 );
5981 },
5982
5983 hideSlideshow: function() {
5984 this.trigger('product_slideshow_hide');
5985
5986 return sections.getInstances('header-section').then(
5987 function(instances) {
5988 instances[0].scrollHandler = true;
5989 this._removeEventHandlers();
5990
5991 a11y$1.removeTrapFocus({ namespace: 'product-slideshow' });
5992
5993 utils.enableScrollBody();
5994 utils.disableTabbingOfChildren(this.$productSlideshow);
5995
5996 this.$productSlideshow
5997 .removeClass(classes$20.showProductSlideshow)
5998 .attr('aria-hidden', true);
5999 this.$slideshowOpenFocus.focus();
6000 }.bind(this)
6001 );
6002 },
6003
6004 setSlide: function(id) {
6005 if (this.slideCount <= 1) return;
6006
6007 var index = this._getSlideIndex(id);
6008
6009 this.trigger('product_slideshow_set_slide', [index]);
6010
6011 return this.slider.setSlide(index); // eslint-disable-line consistent-return
6012 },
6013
6014 _onImageClick: function(evt) {
6015 // By default the image is a link, so prevent going to the image link
6016 evt.preventDefault();
6017 var id = $(evt.currentTarget).data('id');
6018
6019 if (typeof id === 'number') {
6020 this.setSlide(id);
6021 this.showSlideshow();
6022 }
6023 },
6024
6025 _assignEventHandlers: function() {
6026 // Use `.one()` instead of `.on()` because we want to listen and respond
6027 // to one key input at a time. For example, if you hit the forward arrow button
6028 // really fast while the slide is still animating, we don't want it to keep
6029 // animating for all the times that you mashed the key.
6030 $(document).one('keyup.product-slideshow', this._onKeyup.bind(this));
6031
6032 this.$productSlideshow
6033 .on('slidetransitionend', this._setSlideIndicator.bind(this))
6034 .on('click', selectors$22.productSlideshowSelect, this._setSlide.bind(this))
6035 .one(
6036 'click',
6037 selectors$22.productSlideshowClose,
6038 this.hideSlideshow.bind(this)
6039 )
6040 .one('click', selectors$22.productSlideshowNext, this._nextSlide.bind(this))
6041 .one(
6042 'click',
6043 selectors$22.productSlideshowPrevious,
6044 this._previousSlide.bind(this)
6045 );
6046 },
6047
6048 _removeEventHandlers: function() {
6049 this.$productSlideshow.off();
6050 $(document).off('.product-slideshow');
6051 },
6052
6053 _getSlideIndex: function(id) {
6054 return $(selectors$22.productSlideshowSlide, this.$productSlideshow)
6055 .filter('[data-id="' + id + '"]')
6056 .index();
6057 },
6058
6059 _setSlide: function(evt) {
6060 var $navButton = $(evt.target);
6061 var index = $navButton.index();
6062
6063 this.trigger('product_slideshow_set_slide', [index]);
6064 this.slider.setSlide(index, true);
6065 },
6066
6067 _nextSlide: function() {
6068 this.trigger('product_slideshow_next');
6069 this.trigger('product_slideshow_set_slide', [
6070 this.slider.$nextSlide.index()
6071 ]);
6072
6073 return this.slider.nextSlide().then(
6074 function() {
6075 this.$productSlideshow.one(
6076 'click',
6077 selectors$22.productSlideshowNext,
6078 this._nextSlide.bind(this)
6079 );
6080 }.bind(this)
6081 );
6082 },
6083
6084 _previousSlide: function() {
6085 this.trigger('product_slideshow_previous');
6086 this.trigger('product_slideshow_set_slide', [
6087 this.slider.$previousSlide.index()
6088 ]);
6089
6090 return this.slider.previousSlide().then(
6091 function() {
6092 this.$productSlideshow.one(
6093 'click',
6094 selectors$22.productSlideshowPrevious,
6095 this._previousSlide.bind(this)
6096 );
6097 }.bind(this)
6098 );
6099 },
6100
6101 _setSlideIndicator: function(evt, data) {
6102 var index = data.$currentSlide.index();
6103
6104 $(selectors$22.productSlideshowSelect, this.$productSlideshow)
6105 .eq(index)
6106 .addClass(classes$20.productSlideshowSelectActive)
6107 .siblings()
6108 .removeClass(classes$20.productSlideshowSelectActive);
6109 },
6110
6111 _onKeyup: function(evt) {
6112 // Assign the default transition value to be a resolved promise so that if
6113 // we don't end up assigning a real transition (next, prev, esc) then we
6114 // immediately attach another keyup event handler.
6115 var promiseTransition = $.Deferred().resolve();
6116
6117 switch (evt.keyCode) {
6118 case 37:
6119 promiseTransition = this.slider.previousSlide();
6120 break;
6121 case 39:
6122 promiseTransition = this.slider.nextSlide();
6123 break;
6124 case 27:
6125 this.hideSlideshow();
6126 promiseTransition = $.Deferred().reject();
6127 break;
6128 }
6129
6130 promiseTransition.then(
6131 function() {
6132 // When the transition is done, then start listening for a keyup event again
6133 $(document).one('keyup', this._onKeyup.bind(this));
6134 }.bind(this)
6135 );
6136 }
6137};
6138
6139/*
6140
6141Product Template Section
6142--------------------------------------------------------------------------------
6143Shows a product and allows the user to select product variants and add the product
6144to their cart. Adds a product image slideshow that can be viewed when you select
6145an image on the page. Adds expanding social sharing buttons.
6146
6147*/
6148
6149// Extensions
6150// Libs
6151var selectors$21 = {
6152 productFormWrapper: '.product__form-wrapper',
6153 productDescription: '.product__description'
6154};
6155
6156var classes$19 = {
6157 productFormFixed: 'product__form-wrapper--fixed',
6158 productFormBottom: 'product__form-wrapper--bottom'
6159};
6160
6161var formStickyTop = 80;
6162
6163sections.register('product-template', {
6164 onLoad: function() {
6165 this.coverPosition = 'top';
6166
6167 this.extend(productSlideshow);
6168 this.extend(socialSharing);
6169 this.extend(productDisplay);
6170
6171 this.window().on('scroll', this._setFormPosition.bind(this));
6172 this.window().on(
6173 'resize',
6174 debounce(this._callResizeFunctions.bind(this), 500)
6175 );
6176
6177 this.on('variant_add_to_cart', this._updateMeasurements.bind(this));
6178 this.on('variant_change', this._updateHistoryState.bind(this));
6179
6180 this.document().on(
6181 'drawer_open_start',
6182 this._handleFixedSideImage.bind(this)
6183 );
6184 this.document().on(
6185 'drawer_close_done',
6186 this._resetFixedSideImage.bind(this)
6187 );
6188
6189 utils.promiseStylesheet().then(
6190 function() {
6191 this._updateMeasurements();
6192 }.bind(this)
6193 );
6194 },
6195
6196 _callResizeFunctions: function() {
6197 this._updateMeasurements();
6198 this._formatVariantSelectors();
6199 },
6200
6201 _handleFixedSideImage: function() {
6202 if (this.coverPosition !== 'fixed') return;
6203
6204 var scrollTop = $(window).scrollTop();
6205 var translate = scrollTop - this.fixedTop + 2 * formStickyTop + 28;
6206
6207 $(selectors$21.productFormWrapper, this.$container).css({
6208 transform: 'translateY(' + translate + 'px)',
6209 position: 'absolute'
6210 });
6211 },
6212
6213 _resetFixedSideImage: function() {
6214 if (this.coverPosition !== 'fixed') return;
6215
6216 $(selectors$21.productFormWrapper, this.$container).css({
6217 transform: '',
6218 position: ''
6219 });
6220 },
6221
6222 _updateHistoryState: function(event, instance, product, variant) {
6223 if (!history.replaceState) return;
6224
6225 var newurl =
6226 window.location.protocol +
6227 '//' +
6228 window.location.host +
6229 window.location.pathname +
6230 '?variant=' +
6231 variant.id;
6232 window.history.replaceState({ path: newurl }, '', newurl);
6233 },
6234/*
6235 _setFormPosition: function() {
6236 if (utils.isMobile() || !this.enableSticky) return;
6237
6238 var scrollTop = $(window).scrollTop();
6239
6240 // Debounce DOM edits to next frame with requestAnimationFrame
6241 requestAnimationFrame(
6242 function() {
6243 if (scrollTop > this.fixedTop) {
6244 if (scrollTop > this.fixedBottom) {
6245 // Fix to bottom
6246 this.coverPosition = 'bottom';
6247 $(selectors$21.productFormWrapper)
6248 .css('width', this.formWidth)
6249 .removeClass(classes$19.productFormFixed)
6250 .addClass(classes$19.productFormBottom);
6251 } else {
6252 // Fix to the side
6253 this.coverPosition = 'fixed';
6254 $(selectors$21.productFormWrapper)
6255 .css('width', this.formWidth)
6256 .addClass(classes$19.productFormFixed)
6257 .removeClass(classes$19.productFormBottom);
6258 }
6259 } else {
6260 // Fix to the top
6261 this.coverPosition = 'top';
6262 $(selectors$21.productFormWrapper)
6263 .css('width', '')
6264 .removeClass(classes$19.productFormFixed)
6265 .removeClass(classes$19.productFormBottom);
6266 }
6267 }.bind(this)
6268 );
6269 },
6270
6271 _updateMeasurements: function() {
6272 var $formWrapper = $(selectors$21.productFormWrapper);
6273 var descriptionHeight = $(selectors$21.productDescription).outerHeight(true);
6274 var formHeight = $formWrapper.height();
6275
6276 this.enableSticky = descriptionHeight > formHeight;
6277
6278 if (!this.enableSticky) return;
6279
6280 this.fixedTop = $formWrapper.parent().offset().top - formStickyTop;
6281 this.fixedBottom = this.fixedTop + descriptionHeight - formHeight;
6282 this.formWidth = $formWrapper.parent().width();
6283
6284 this._setFormPosition();
6285 }*/
6286});
6287
6288var selectors$23 = {
6289 slider: '.quotes-slider',
6290 slide: '.quotes-slide'
6291};
6292
6293var classes$21 = {
6294 visible: 'quotes-slide--visible'
6295};
6296
6297sections.register('quotes-section', {
6298 onLoad: function() {
6299 this.$container = $(this.container);
6300 this.$slider = $(selectors$23.slider, this.$container);
6301 this.totalSlides = this.$slider.data('count');
6302 this.speed = this.$slider.data('speed');
6303 this.currentSlide = 0;
6304
6305 if (this.$slider.find(selectors$23.slide).length > 1) {
6306 this._startSlider();
6307 }
6308 },
6309
6310 _startSlider: function() {
6311 this.interval = setInterval(
6312 function() {
6313 if (this.currentSlide + 1 >= this.totalSlides) {
6314 this.currentSlide = 0;
6315 } else {
6316 this.currentSlide++;
6317 }
6318
6319 this._showSlide(selectors$23.slide + '--' + this.currentSlide);
6320 }.bind(this),
6321 this.speed
6322 );
6323 },
6324
6325 _showSlide: function(slide) {
6326 this.$slider
6327 .find(selectors$23.slide)
6328 .removeClass(classes$21.visible)
6329 .end()
6330 .find(slide)
6331 .addClass(classes$21.visible);
6332 },
6333
6334 onBlockSelect: function(evt) {
6335 clearInterval(this.interval);
6336
6337 var $selectedSlide = this.$container.find(
6338 selectors$23.slide + '--' + evt.detail.blockId
6339 );
6340
6341 this._showSlide($selectedSlide);
6342 },
6343
6344 onBlockDeselect: function() {
6345 this._startSlider();
6346 }
6347});
6348
6349/*
6350
6351Search Template Section
6352--------------------------------------------------------------------------------
6353Creates a staggered card grid for the search template.
6354
6355*/
6356
6357// Extensions
6358// Libs
6359sections.register('search-template', {
6360 onLoad: function() {
6361 this.extend(cardManager);
6362 }
6363});
6364
6365var promiseYoutubeAPI;
6366
6367var youtube = {
6368 promiseAPI: function() {
6369 if (!promiseYoutubeAPI) {
6370 var tag = document.createElement('script');
6371
6372 tag.src = 'https://www.youtube.com/iframe_api';
6373 var firstScriptTag = document.getElementsByTagName('script')[0];
6374 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
6375
6376 promiseYoutubeAPI = $.Deferred(function(defer) {
6377 window.onYouTubeIframeAPIReady = defer.resolve;
6378
6379 setTimeout(function() {
6380 defer.reject('Request for YouTube API timed out after 30 seconds.');
6381 }, 30000);
6382 });
6383 }
6384
6385 return promiseYoutubeAPI;
6386 },
6387
6388 promisePlayer: function(id, options) {
6389 return this.promiseAPI().then(function() {
6390 return $.Deferred(function(defer) {
6391 if (typeof window.YT === 'undefined') {
6392 defer.reject(
6393 "We're sorry, something went wrong. The YouTube API has not loaded correctly."
6394 );
6395 }
6396
6397 /* eslint-disable no-undef */
6398 var player = new YT.Player(id, options); // global YT variable injected by YouTube API
6399
6400 player.addEventListener('onReady', function() {
6401 defer.resolve(player);
6402 });
6403
6404 setTimeout(function() {
6405 defer.reject(
6406 'Request for YouTube player has timed out after 30 seconds.'
6407 );
6408 }, 30000);
6409 });
6410 });
6411 }
6412};
6413
6414var promiseVimeoAPI;
6415
6416var vimeo = {
6417 promiseAPI: function() {
6418 if (!promiseVimeoAPI) {
6419 promiseVimeoAPI = $.Deferred(function(defer) {
6420 var tag = document.createElement('script');
6421 tag.src = 'https://player.vimeo.com/api/player.js';
6422 tag.onload = tag.onreadystatechange = function() {
6423 if (!this.readyState || this.readyState === 'complete') {
6424 defer.resolve();
6425 }
6426 };
6427
6428 var firstScriptTag = document.getElementsByTagName('script')[0];
6429 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
6430
6431 setTimeout(function() {
6432 defer.reject('Request for Vimeo API timed out after 30 seconds.');
6433 }, 30000);
6434 });
6435 }
6436
6437 return promiseVimeoAPI;
6438 },
6439
6440 promisePlayer: function(id, options) {
6441 return this.promiseAPI().then(function() {
6442 return $.Deferred(function(defer) {
6443 if (typeof window.Vimeo === 'undefined') {
6444 defer.reject(
6445 "We're sorry, something went wrong. The Vimeo API has not loaded correctly."
6446 );
6447 }
6448
6449 var player = new window.Vimeo.Player(id, options);
6450
6451 setTimeout(function() {
6452 defer.reject(
6453 'Request for Vimeo player has timed out after 30 seconds.'
6454 );
6455 }, 30000);
6456
6457 player.ready().then(function() {
6458 defer.resolve(player);
6459 });
6460 });
6461 });
6462 }
6463};
6464
6465/*
6466
6467Slideshow Desktop Extension
6468--------------------------------------------------------------------------------
6469Manages all the desktop behaviour of the home page slideshow
6470
6471
6472Events
6473------------
6474
6475Name: slideshow_desktop_init_start
6476Description: Fired before the desktop slideshow begins to initialize
6477Payload: none
6478
6479Name: slideshow_desktop_init_done
6480Description: Fired when the desktop slideshow is done initializing
6481Payload: none
6482
6483Name: slideshow_desktop_destroy
6484Description: Fired when the desktop slideshow is destroyed
6485Payload: none
6486
6487Name: slideshow_set_slide
6488Description: Fired when the user selects a specific slide
6489Payload: { number } Index of the slide being displayed
6490
6491Name: slideshow_previous_slide
6492Description: Fired when the user selects the previous slide
6493Payload: { number } Index of the slide being displayed
6494
6495Name: slideshow_next_slide
6496Description: Fired when the user selects the next slide
6497Payload: { number } Index of the slide being displayed
6498
6499Name: slideshow_video_load
6500Description: Fired when a video is being loaded in the slideshow
6501Payload: { object } Video player DOM object
6502
6503Name: slideshow_video_loaded
6504Description: Fired when the video is done loading in the slideshow
6505Payload: { object } Video player DOM object
6506
6507*/
6508
6509var selectors$25 = {
6510 buttons: '.slideshow__buttons',
6511 button: '.slideshow__button',
6512 pauseButton: '[data-pause-button]',
6513 label: '.slideshow__button-label',
6514 cta: '.slideshow__button-cta--desktop',
6515 ctaText: '.slideshow__heading-cta-text',
6516 slide: '.slideshow__slide',
6517 slideshow: '.slideshow',
6518 video: '.slideshow__video'
6519};
6520
6521var classes$23 = {
6522 centredCta: 'slideshow--center-cta',
6523 buttonActive: 'slideshow__button--active',
6524 headingCtaActive: 'slideshow__heading-cta-text--active',
6525 headingCtaTransitioning: 'slideshow__heading-cta-text--transitioning',
6526 linkActive: 'slideshow__button--link',
6527 slideActive: 'slideshow__slide--active',
6528 slideActiveTransitioning: 'slideshow__slide--transitioning',
6529 videoLoaded: 'slideshow__video--loaded',
6530 videoPaused: 'slideshow__video--paused',
6531 paused: 'is-paused'
6532};
6533
6534var config = {
6535 // Intensity for desktop mouse over effect (if more than 3 slides)
6536 easeIntensity: 10
6537};
6538
6539var slideshowDesktop = {
6540 initDesktopSlideshow: function() {
6541 this.trigger('slideshow_desktop_init_start');
6542
6543 this.$slideshow = $(selectors$25.slideshow, this.$container);
6544 this.$slide = $(selectors$25.slide, this.$container);
6545 this.$buttons = $(selectors$25.buttons, this.$container);
6546 this.$button = $(selectors$25.button, this.$container);
6547
6548 this.desktopSlideshow = true;
6549 this.isAnimating = false;
6550 this.currentDesktopSlide = 0;
6551 this.totalSlides = this.$buttons.data('count');
6552 this.players = [];
6553 this.desktopSlideshowNamespace = '.desktopSlideshow';
6554
6555 this.on(
6556 'mouseover' + this.desktopSlideshowNamespace,
6557 this._onHoverSlideshow.bind(this)
6558 );
6559 this.on(
6560 'mousemove' + this.desktopSlideshowNamespace,
6561 this._mouseMoveButtons.bind(this)
6562 );
6563 this.on(
6564 'mouseleave' + this.desktopSlideshowNamespace,
6565 this._resetButtonsPosition.bind(this)
6566 );
6567 this.on(
6568 'keydown' + this.desktopSlideshowNamespace,
6569 this._onTabButtons.bind(this)
6570 );
6571 this.on(
6572 'click' + this.desktopSlideshowNamespace,
6573 selectors$25.pauseButton,
6574 this._onPauseButton.bind(this)
6575 );
6576 this.on(
6577 'click' + this.desktopSlideshowNamespace,
6578 selectors$25.label,
6579 this._onClickButton.bind(this)
6580 );
6581 this.on(
6582 'keydown' + this.desktopSlideshowNamespace,
6583 this._addKeyBindingsDesktop.bind(this)
6584 );
6585
6586 this.window().on(
6587 'resize' + this.desktopSlideshowNamespace,
6588 this._setButtonWrapperValues.bind(this)
6589 );
6590
6591 utils.promiseStylesheet().then(
6592 function() {
6593 this._setButtonWrapperValues();
6594 this._setSlideDesktop(0);
6595 if (this.$container.hasClass(classes$23.centredCta)) {
6596 this._setButtonStatus(0);
6597 }
6598 this.trigger('slideshow_desktop_init_done');
6599 }.bind(this)
6600 );
6601 },
6602
6603 destroyDesktopSlideshow: function() {
6604 this.trigger('slideshow_desktop_destroy');
6605
6606 this.desktopSlideshow = false;
6607 this.off(this.desktopSlideshowNamespace);
6608 this.window().off(this.desktopSlideshowNamespace);
6609
6610 this._setButtonsTranslateX(0);
6611
6612 // Loop over every video slide that is found as part of this.players
6613 // and explicitly call the YouTube and/or Vimeo destroy method
6614 // depending on the type of video player.
6615 for (var key in this.players) {
6616 if (!this.players.hasOwnProperty(key)) return;
6617
6618 var player = this.players[key];
6619
6620 if (typeof player.destroy === 'function') {
6621 player.destroy();
6622 } else if (typeof player.unload === 'function') {
6623 player.unload();
6624 }
6625 }
6626
6627 this.players = [];
6628 },
6629
6630 _onHoverSlideshow: function() {
6631 this._animateButtonFrame();
6632 },
6633
6634 _mouseMoveButtons: function(evt) {
6635 if (this.totalSlides <= 3) return;
6636
6637 this.mousePosition = evt.pageX - this.centerOfButtonsWrapper;
6638
6639 if (!this.isAnimating) {
6640 this.isAnimating = true;
6641 this._animateButtonFrame();
6642 }
6643 },
6644
6645 _resetButtonsPosition: function() {
6646 this.mousePosition = 0;
6647 },
6648
6649 _onTabButtons: function(evt) {
6650 if (evt.which !== utils.keyboardKeys.TAB) return;
6651
6652 var $nextButton;
6653
6654 if (!evt.shiftKey) {
6655 $nextButton = $(evt.target)
6656 .closest(this.$button)
6657 .next();
6658 } else {
6659 $nextButton = $(evt.target)
6660 .closest(this.$button)
6661 .prev();
6662 }
6663
6664 if (!$nextButton.length) return;
6665
6666 var division = $(window).width() / this.totalSlides;
6667 var buttonIndex = $nextButton.index();
6668
6669 this.mousePosition = division * buttonIndex - $nextButton.width();
6670 this._animateButtonFrame();
6671 },
6672
6673 _onPauseButton: function(evt) {
6674 var $currentSlide = this.$slide.eq(this.currentDesktopSlide);
6675 var $pauseButton = $(evt.target);
6676 var isPaused = $pauseButton.hasClass(classes$23.paused);
6677 var blockId = this.$video.attr('data-block-id');
6678
6679 $pauseButton.toggleClass(classes$23.paused, !isPaused).attr({
6680 'aria-label': isPaused
6681 ? $pauseButton.data('label-pause')
6682 : $pauseButton.data('label-play'),
6683 'aria-pressed': !isPaused
6684 });
6685
6686 if (this.players[blockId]) {
6687 if (isPaused) {
6688 this.players[blockId].playVideo();
6689 } else {
6690 this.players[blockId].pauseVideo();
6691 }
6692 }
6693 $currentSlide.toggleClass(classes$23.videoPaused);
6694 },
6695
6696 _onClickButton: function(evt) {
6697 var $buttonClicked = $(evt.target).closest(selectors$25.button);
6698
6699 if ($buttonClicked.hasClass(classes$23.buttonActive)) return;
6700
6701 var index = $buttonClicked.index();
6702
6703 this._setSlideDesktop(index);
6704
6705 // Allow first slide to be clicked after initial load
6706 if (!$buttonClicked.hasClass(classes$23.buttonActive)) {
6707 this._setButtonStatus(index);
6708 }
6709 },
6710
6711 _setButtonStatus: function(index) {
6712 var $button = this.$button.eq(index);
6713 var $buttonText = $button.find(selectors$25.ctaText);
6714
6715 this.$button
6716 .removeClass(classes$23.buttonActive)
6717 .find(selectors$25.label)
6718 .attr('aria-expanded', false);
6719
6720 $button
6721 .addClass(classes$23.buttonActive)
6722 .find(selectors$25.label)
6723 .attr('aria-expanded', true)
6724 .focus();
6725
6726 this.$buttons
6727 .find(selectors$25.ctaText)
6728 .empty()
6729 .attr('aria-hidden', true)
6730 .removeClass(classes$23.headingCtaActive)
6731 .removeAttr('style');
6732
6733 if ($buttonText.parent().is('a')) {
6734 this._showButtonContent($buttonText);
6735 }
6736
6737 this.$button.find(selectors$25.cta).attr('tabindex', '-1');
6738 if ($button.hasClass(classes$23.linkActive)) {
6739 $button.find(selectors$25.cta).attr('tabindex', '0');
6740 }
6741 },
6742
6743 _animateButtonFrame: function() {
6744 var deltaPosition = this.mousePosition - this.xPosition;
6745 // Calculates the differential ratio between the width of the button wrapper
6746 // and the overflowed button element that actually animates.
6747 var widthDifferenceRatio =
6748 this.buttonsInnerWidth / this.buttonsWrapperWidth - 1;
6749
6750 // deltaPosition represents the momentum and will increment down every frame
6751 // until it reaches < 1, the buttons will then stop animating.
6752 if (Math.abs(deltaPosition) < 1) {
6753 this.isAnimating = false;
6754 return;
6755 }
6756
6757 // This represents the mouse position relative to the slideshow width
6758 // where this.xPosition equals 0 if the mouse position is in the center.
6759 this.xPosition += deltaPosition / config.easeIntensity;
6760
6761 this._setButtonsTranslateX(-(this.xPosition * widthDifferenceRatio));
6762
6763 // This function is called recursively until the condition above is met,
6764 // meaning once the buttons have stopped animating.
6765 requestAnimationFrame(this._animateButtonFrame.bind(this));
6766 },
6767
6768 _showButtonContent: function($buttonText) {
6769 var buttonText = $buttonText.data('button-text');
6770
6771 $buttonText.html(buttonText).attr('aria-hidden', false);
6772
6773 if (
6774 !$buttonText.attr('data-new-width') ||
6775 !$buttonText.attr('data-new-height')
6776 ) {
6777 $buttonText.attr('data-new-width', $buttonText.outerWidth());
6778 $buttonText.attr('data-new-height', $buttonText.outerHeight());
6779 }
6780
6781 var newButtonContentWidth = $buttonText.attr('data-new-width');
6782 var newButtonContentHeight = $buttonText.attr('data-new-height');
6783
6784 $buttonText.empty();
6785
6786 requestAnimationFrame(function() {
6787 $buttonText
6788 .css({
6789 minHeight: newButtonContentHeight + 'px',
6790 minWidth: newButtonContentWidth + 'px'
6791 })
6792 .addClass(classes$23.headingCtaTransitioning);
6793 });
6794
6795 utils.promiseTransitionEnd($buttonText).then(function() {
6796 $buttonText
6797 .html(buttonText)
6798 .removeClass(classes$23.headingCtaTransitioning)
6799 .addClass(classes$23.headingCtaActive);
6800 });
6801 },
6802
6803 _setButtonsTranslateX: function(xPosition) {
6804 this.$buttons.css({
6805 transform: 'translate3d(' + xPosition + 'px, 0, 0)'
6806 });
6807 },
6808
6809 _addKeyBindingsDesktop: function(evt) {
6810 if (evt.which === utils.keyboardKeys.LEFTARROW) {
6811 this._previousSlideDesktop();
6812 } else if (evt.which === utils.keyboardKeys.RIGHTARROW) {
6813 this._nextSlideDesktop();
6814 }
6815 },
6816
6817 _previousSlideDesktop: function() {
6818 if (this.currentDesktopSlide === 0) return;
6819
6820 this._setSlideDesktop(this.currentDesktopSlide - 1);
6821 },
6822
6823 _nextSlideDesktop: function() {
6824 if (this.currentDesktopSlide === this.totalSlides - 1) return;
6825
6826 this._setSlideDesktop(this.currentDesktopSlide + 1);
6827 },
6828
6829 _setSlideDesktop: function(slideIndex) {
6830 var $currentSlide = this.$slide.eq(this.currentDesktopSlide);
6831 var $nextSlide = this.$slide.eq(slideIndex);
6832 var $video = $nextSlide.find(selectors$25.video);
6833
6834 // We call _loadVideo() before we check to see if
6835 // this.currentDesktopSlide === slideIndex (below). This would never fire
6836 // on initial load if it was after the condition below since 0 === 0
6837 // would return true.
6838 if ($video.length) {
6839 this._loadVideo($video, $nextSlide);
6840 }
6841
6842 if (this.currentDesktopSlide === slideIndex) return;
6843
6844 $nextSlide.addClass(classes$23.slideActive).attr('aria-hidden', false);
6845 $currentSlide.addClass(classes$23.slideActiveTransitioning);
6846
6847 utils.promiseTransitionEnd($nextSlide).then(function() {
6848 $currentSlide
6849 .removeClass(classes$23.slideActive)
6850 .removeClass(classes$23.slideActiveTransitioning)
6851 .attr('aria-hidden', true);
6852 });
6853
6854 this._setButtonStatus(slideIndex);
6855
6856 this.currentDesktopSlide = slideIndex;
6857
6858 this.trigger('slideshow_set_slide', [slideIndex]);
6859
6860 if (this.currentDesktopSlide - 1 >= 0) {
6861 this.trigger('slideshow_previous_slide', [slideIndex - 1]);
6862 }
6863
6864 if (this.currentDesktopSlide + 1 < this.totalSlides) {
6865 this.trigger('slideshow_next_slide', [slideIndex + 1]);
6866 }
6867 },
6868
6869 _loadVideo: function($video, $slide) {
6870 this.$video = $video;
6871
6872 this.trigger('slideshow_video_load', [$video[0]]);
6873
6874 return this._promiseVideo().then(
6875 function() {
6876 $slide.addClass(classes$23.videoLoaded);
6877 $slide.find(selectors$25.pauseButton).prop('disabled', false);
6878 this.trigger('slideshow_video_loaded', [$video[0]]);
6879 }.bind(this)
6880 );
6881 },
6882
6883 _promiseVideo: function() {
6884 var playerType = this.$video.attr('data-video-type');
6885 var promiseVideoPlayer;
6886
6887 if (playerType === 'youtube') {
6888 promiseVideoPlayer = this._loadYoutubePlayer();
6889 this.$video.attr('tabindex', '-1');
6890 } else if (playerType === 'vimeo') {
6891 promiseVideoPlayer = this._loadVimeoPlayer();
6892 this.$video.find('iframe').attr('tabindex', '-1');
6893 }
6894
6895 return promiseVideoPlayer;
6896 },
6897
6898 _loadYoutubePlayer: function() {
6899 var blockId = this.$video.attr('data-block-id');
6900 var videoId = this.$video.attr('data-video-id');
6901
6902 return youtube
6903 .promisePlayer(this.$video[0], {
6904 videoId: videoId,
6905 ratio: 16 / 9,
6906 playerVars: {
6907 // eslint-disable-next-line camelcase
6908 iv_load_policy: 3,
6909 modestbranding: 1,
6910 autoplay: 0,
6911 controls: 0,
6912 showinfo: 0,
6913 wmode: 'opaque',
6914 branding: 0,
6915 autohide: 0,
6916 rel: 0
6917 },
6918 events: {
6919 onStateChange: function(evt) {
6920 // Video has ended, loop back to beginning
6921 if (evt.data === 0) {
6922 this.players[blockId].seekTo(0);
6923 }
6924 }.bind(this)
6925 }
6926 })
6927 .then(
6928 function(player) {
6929 this.players[blockId] = player;
6930 player.playVideo().mute();
6931 // The video will not play if the iframe is set to visibility: hidden
6932 // Need to set it seperately from other styles in order to resolve the promise
6933 $(player.a).css('visibility', 'visible');
6934 // set player to visible
6935 return $.Deferred(function(defer) {
6936 player.addEventListener('onStateChange', function(evt) {
6937 // Only resolve the promise if the video is playing
6938 if (evt.data === 1) {
6939 defer.resolve();
6940 }
6941 });
6942 });
6943 }.bind(this)
6944 );
6945 },
6946
6947 _loadVimeoPlayer: function() {
6948 var blockId = this.$video.attr('data-block-id');
6949 var videoId = this.$video.attr('data-video-id');
6950
6951 return vimeo
6952 .promisePlayer(this.$video[0], {
6953 id: videoId,
6954 loop: true,
6955 // This property isn't reliable. The user might see the Vimeo playbar flash
6956 // as the video begins to play.
6957 playbar: false,
6958 background: true
6959 })
6960 .then(
6961 function(player) {
6962 this.players[blockId] = player;
6963 player.play();
6964 player.setVolume(0);
6965
6966 return $.Deferred(function(defer) {
6967 player.on('loaded', function() {
6968 defer.resolve();
6969 });
6970 });
6971 }.bind(this)
6972 );
6973 },
6974
6975 _setButtonWrapperValues: function() {
6976 this.mousePosition = 0;
6977 this.xPosition = 0;
6978 this.buttonsWrapperWidth = this.$container.outerWidth();
6979 this.buttonsInnerWidth = this.$button.first().width() * this.totalSlides;
6980 this.centerOfButtonsWrapper = this.buttonsWrapperWidth / 2;
6981 this.$button
6982 .find(selectors$25.ctaText)
6983 .removeAttr(
6984 'style data-previous-width data-previous-height data-new-width data-new-height'
6985 );
6986
6987 this._setButtonsTranslateX(0);
6988 }
6989};
6990
6991/*
6992
6993Slideshow Mobile Extension
6994--------------------------------------------------------------------------------
6995Manages all the mobile behaviour of the home page slideshow
6996
6997
6998Events
6999------------
7000
7001Name: slideshow_mobile_init_start
7002Description: Fired before the mobile slideshow begins to initialize
7003Payload: none
7004
7005Name: slideshow_mobile_init_done
7006Description: Fired when the mobile slideshow is done initializing
7007Payload: none
7008
7009Name: slideshow_mobile_destroy
7010Description: Fired when the mobile slideshow is destroyed
7011Payload: none
7012
7013Name: slideshow_set_slide
7014Description: Fired when the user selects a specific slide
7015Payload: { number } Index of the slide being displayed
7016
7017Name: slideshow_previous_slide
7018Description: Fired when the user selects the previous slide
7019Payload: { number } Index of the slide being displayed
7020
7021Name: slideshow_next_slide
7022Description: Fired when the user selects the next slide
7023Payload: { number } Index of the slide being displayed
7024
7025*/
7026
7027var selectors$26 = {
7028 button: '.slideshow__button',
7029 buttons: '.slideshow__buttons',
7030 ctaMultipleSlides: '.slideshow__button-cta--multiple',
7031 ctaSingleSlide: '.slideshow__button-cta-single',
7032 label: '.slideshow__button-label',
7033 mobileTextContainer: '.slideshow__text-container-mobile',
7034 mobileTextContent: '.slideshow__text-content-mobile',
7035 navigationButtons: '[data-slider-navigation]',
7036 nextButton: '[data-slider-navigation-next]',
7037 previousButton: '[data-slider-navigation-previous]',
7038 slide: '.slideshow__slide',
7039 slideshow: '.slideshow',
7040 indicatorDots: '.slideshow__indicator'
7041};
7042
7043var classes$24 = {
7044 buttonActive: 'slideshow__button--active',
7045 dotActive: 'slideshow__indicator--active',
7046 linkActive: 'slideshow__button--link',
7047 slideActive: 'slideshow__slide--active',
7048 slideActiveTransitioning: 'slideshow__slide--transitioning',
7049 navigationNoFocus: 'slideshow__navigation-item--no-focus'
7050};
7051
7052var slideshowMobile = {
7053 initMobileSlideshow: function() {
7054 this.trigger('slideshow_mobile_init_start');
7055
7056 this.$slideshow = $(selectors$26.slideshow, this.$container);
7057 this.$buttons = $(selectors$26.buttons, this.$container);
7058 this.$button = $(selectors$26.button, this.$container);
7059 this.$navigationButtons = $(selectors$26.navigationButtons, this.$container);
7060 this.$ctaMultipleSlides = $(selectors$26.ctaMultipleSlides, this.$container);
7061 this.$ctaSingleSlide = $(selectors$26.ctaSingleSlide, this.$container);
7062 this.$indicatorDots = $(selectors$26.indicatorDots, this.$container);
7063 this.$mobileTextContainer = $(
7064 selectors$26.mobileTextContainer,
7065 this.$container
7066 );
7067 this.$mobileTextContent = $(selectors$26.mobileTextContent, this.$container);
7068
7069 this.mobileSlideshow = true;
7070 this.currentMobileSlide = 0;
7071 this.totalSlides = this.$buttons.data('count');
7072 this.xPosition = 0;
7073 this.mobileSlideshowNamespace = '.mobileSlideshow';
7074
7075 // The header is above the slideshow in the iOS editor, so we need to
7076 // reduce it's height by the height of the header.
7077 if ($('html').hasClass('is-ios') && Shopify.designMode) {
7078 this.$slideshow.css('height', '-=60px');
7079 }
7080
7081 this.on(
7082 'click keyup' + this.mobileSlideshowNamespace,
7083 selectors$26.indicatorDots,
7084 this._onClickIndicatorDot.bind(this)
7085 );
7086 this.on(
7087 'click keyup' + this.mobileSlideshowNamespace,
7088 selectors$26.previousButton,
7089 this._previousSlideMobile.bind(this)
7090 );
7091 this.on(
7092 'click keyup' + this.mobileSlideshowNamespace,
7093 selectors$26.nextButton,
7094 this._nextSlideMobile.bind(this)
7095 );
7096 this.on(
7097 'keydown' + this.mobileSlideshowNamespace,
7098 this._addKeyBindingsMobile.bind(this)
7099 );
7100
7101 if (this.totalSlides > 1) {
7102 this.hammertime = new Hammer(this.$container[0]);
7103
7104 // Import swipe gestures and only allow these two events
7105 this.hammertime
7106 .on('swipeleft', this._nextSlideMobile.bind(this))
7107 .on('swiperight', this._previousSlideMobile.bind(this));
7108 }
7109
7110 this.$button.first().addClass(classes$24.buttonActive);
7111
7112 utils.promiseStylesheet().then(
7113 function() {
7114 this._setSlideMobile(0);
7115 this._setMobileText(0);
7116 this._setSlideshowA11y();
7117 this.trigger('slideshow_mobile_init_done');
7118 }.bind(this)
7119 );
7120 },
7121
7122 destroyMobileSlideshow: function() {
7123 this.trigger('slideshow_mobile_destroy');
7124
7125 this.mobileSlideshow = false;
7126 this.$container.off(this.mobileSlideshowNamespace);
7127
7128 if (this.totalSlides > 1) {
7129 this.hammertime.destroy();
7130 }
7131 },
7132
7133 _onClickIndicatorDot: function(evt) {
7134 var $indicatorDot = $(evt.target);
7135 var index = $indicatorDot.data('slide-index');
7136
7137 evt.preventDefault();
7138
7139 if (
7140 evt.type === 'keyup' &&
7141 !(
7142 evt.keyCode === utils.keyboardKeys.ENTER ||
7143 evt.keyCode === utils.keyboardKeys.SPACE
7144 )
7145 )
7146 return;
7147
7148 this._setSlideMobile(index);
7149
7150 if (evt.type === 'keyup' || evt.detail === 0) {
7151 this.$slideshow.focus();
7152 }
7153 },
7154
7155 _addKeyBindingsMobile: function(evt) {
7156 if (evt.which === utils.keyboardKeys.LEFTARROW) {
7157 this._previousSlideMobile(evt);
7158 } else if (evt.which === utils.keyboardKeys.RIGHTARROW) {
7159 this._nextSlideMobile(evt);
7160 }
7161 },
7162
7163 _previousSlideMobile: function(evt) {
7164 if (evt.type === 'click') {
7165 $(evt.target).addClass(classes$24.navigationNoFocus);
7166 }
7167 if (
7168 (evt.type === 'keyup' &&
7169 !(
7170 evt.keyCode === utils.keyboardKeys.ENTER ||
7171 evt.keyCode === utils.keyboardKeys.SPACE
7172 )) ||
7173 this.currentMobileSlide === 0
7174 ) {
7175 return;
7176 }
7177
7178 this._setSlideMobile(this.currentMobileSlide - 1);
7179 },
7180
7181 _nextSlideMobile: function(evt) {
7182 if (evt.type === 'click') {
7183 $(evt.target).addClass(classes$24.navigationNoFocus);
7184 }
7185 if (
7186 (evt.type === 'keyup' &&
7187 !(
7188 evt.keyCode === utils.keyboardKeys.ENTER ||
7189 evt.keyCode === utils.keyboardKeys.SPACE
7190 )) ||
7191 this.currentMobileSlide === this.totalSlides - 1
7192 ) {
7193 return;
7194 }
7195
7196 this._setSlideMobile(this.currentMobileSlide + 1);
7197 },
7198
7199 _setSlideMobile: function(slideIndex) {
7200 if (this.currentMobileSlide === slideIndex) return;
7201
7202 this.xPosition = slideIndex * 50;
7203
7204 this.$buttons.css({
7205 transform: 'translate3d(-' + this.xPosition + '%, 0, 0)'
7206 });
7207
7208 this._setActiveStates(slideIndex);
7209 this._setSlideA11y(slideIndex);
7210 this._setMobileText(slideIndex);
7211
7212 this.currentMobileSlide = slideIndex;
7213
7214 this.trigger('slideshow_set_slide', [slideIndex]);
7215
7216 this.$navigationButtons.attr('disabled', false);
7217
7218 if (this.currentMobileSlide === 0) {
7219 this.$navigationButtons
7220 .filter(selectors$26.previousButton)
7221 .attr('disabled', true);
7222 }
7223
7224 if (this.currentMobileSlide === this.totalSlides - 1) {
7225 this.$navigationButtons
7226 .not(selectors$26.previousButton)
7227 .attr('disabled', true);
7228 }
7229
7230 if (this.currentMobileSlide - 1 >= 0) {
7231 this.trigger('slideshow_previous_slide', [slideIndex - 1]);
7232 }
7233
7234 if (this.currentMobileSlide + 1 < this.totalSlides) {
7235 this.trigger('slideshow_next_slide', [slideIndex + 1]);
7236 }
7237 },
7238
7239 _setActiveStates: function(slideIndex) {
7240 this.$slide = this.$slide || $(selectors$26.slide, this.$container); // eslint-disable-line shopify/jquery-dollar-sign-reference
7241 this.$button = this.$button || $(selectors$26.button, this.$container); // eslint-disable-line shopify/jquery-dollar-sign-reference
7242 this.$dot = this.$dot || $(selectors$26.indicatorDots, this.$container); // eslint-disable-line shopify/jquery-dollar-sign-reference
7243
7244 var $currentSlide = this.$slide.eq(this.currentMobileSlide);
7245 var $nextSlide = this.$slide.eq(slideIndex);
7246
7247 $nextSlide.addClass(classes$24.slideActive).attr('aria-hidden', false);
7248 $currentSlide.addClass(classes$24.slideActiveTransitioning);
7249
7250 utils.promiseTransitionEnd($nextSlide).then(function() {
7251 $currentSlide
7252 .removeClass(classes$24.slideActive)
7253 .removeClass(classes$24.slideActiveTransitioning)
7254 .attr('aria-hidden', true);
7255 });
7256
7257 this.$button.removeClass(classes$24.buttonActive);
7258
7259 this.$button.eq(slideIndex).addClass(classes$24.buttonActive);
7260
7261 this.$dot.removeClass(classes$24.dotActive);
7262 this.$dot.eq(slideIndex).addClass(classes$24.dotActive);
7263 },
7264
7265 _setSlideshowA11y: function() {
7266 this.$labels = this.$labels || this.$button.find(selectors$26.label); // eslint-disable-line shopify/jquery-dollar-sign-reference
7267 this.$ctaSingleSlide =
7268 this.$ctaSingleSlide || this.$button.find(selectors$26.ctaSingleSlide); // eslint-disable-line shopify/jquery-dollar-sign-reference
7269
7270 this.$ctaSingleSlide.attr('tabindex', '0');
7271 this.$labels.attr('tabindex', '-1');
7272 this._setSlideA11y(0);
7273
7274 $.each(
7275 this.$indicatorDots,
7276 function(index, indicatorDot) {
7277 $(indicatorDot).attr({
7278 'aria-controls': 'Slide' + index
7279 });
7280 }.bind(this)
7281 );
7282 },
7283
7284 _setSlideA11y: function(slideIndex) {
7285 var $button = this.$button.eq(slideIndex);
7286
7287 this.$ctasMultipleSlides =
7288 this.$ctasMultipleSlides ||
7289 this.$button.find(selectors$26.ctaMultipleSlides); // eslint-disable-line shopify/jquery-dollar-sign-reference
7290
7291 if (this.$ctasMultipleSlides) {
7292 this.$ctasMultipleSlides.attr('tabindex', '-1');
7293
7294 // All slide titles are tabbable. If the currently active button has a CTA
7295 // link, the CTA link becomes tabbable as well.
7296 if ($button.hasClass(classes$24.linkActive)) {
7297 this.$ctasMultipleSlides.eq(slideIndex).attr('tabindex', '0');
7298 }
7299 }
7300
7301 $.each(
7302 this.$indicatorDots,
7303 function(index, indicatorDot) {
7304 $(indicatorDot).attr({
7305 'aria-label': this._slideLabel(slideIndex, index),
7306 'aria-current': slideIndex === index ? true : false
7307 });
7308 }.bind(this)
7309 );
7310 },
7311
7312 _setMobileText: function(slideIndex) {
7313 var $currentTextContent = this.$mobileTextContent.eq(slideIndex);
7314 this.$ctaSingleSlide =
7315 this.$ctaSingleSlide || this.$button.find(selectors$26.ctaSingleSlide); // eslint-disable-line shopify/jquery-dollar-sign-reference
7316
7317 if (this.$ctaSingleSlide.length) {
7318 // Adjust for buttons with labels on multiple lines.
7319 var paddingAdjustment =
7320 (this.$ctaSingleSlide.outerHeight() - 50) / 2 + 40;
7321 this.$mobileTextContent.css('padding-top', paddingAdjustment + 'px');
7322 }
7323
7324 this.$mobileTextContent.hide();
7325 $currentTextContent.show();
7326 },
7327
7328 _slideLabel: function(activeSlideIndex, currentIndex) {
7329 var label =
7330 activeSlideIndex === currentIndex
7331 ? theme.strings.slideshow.activeSlideA11yString
7332 : theme.strings.slideshow.loadSlideA11yString;
7333
7334 return label.replace('[slide_number]', currentIndex + 1);
7335 }
7336};
7337
7338/*
7339
7340Slideshow Section
7341--------------------------------------------------------------------------------
7342Manages the functionality of the both mobile and desktop slideshow
7343
7344*/
7345
7346// Extensions
7347// Libs
7348var selectors$24 = {
7349 button: '.slideshow__button',
7350 slide: '.slideshow__slide'
7351};
7352
7353var classes$22 = {
7354 buttonActive: 'slideshow__button--active',
7355 slideActive: 'slideshow__slide--active'
7356};
7357
7358sections.register('slideshow-section', {
7359 onLoad: function() {
7360 this.extend(slideshowDesktop);
7361 this.extend(slideshowMobile);
7362
7363 this.$container.on('focusin' + this.namespace, this._onFocusIn.bind(this));
7364 this.$container.on(
7365 'focusout' + this.namespace,
7366 this._onFocusOut.bind(this)
7367 );
7368 this._toggleViewState();
7369
7370 this.window().on('resize', this._toggleViewState.bind(this));
7371 },
7372
7373 onUnload: function() {
7374 this._destroyDesktopState();
7375 this._destroyMobileState();
7376 },
7377
7378 onBlockSelect: function(evt) {
7379 utils.promiseStylesheet().then(
7380 function() {
7381 var index = $(evt.target).index();
7382
7383 if (this.mobileViewEnabled) {
7384 this._setSlideMobile(index);
7385 } else {
7386 this._setSlideDesktop(index);
7387 }
7388 }.bind(this)
7389 );
7390 },
7391
7392 _toggleViewState: function() {
7393 var windowWidth = $(window).innerWidth();
7394 var enableMobileView;
7395 var enableDesktopView;
7396
7397 if (typeof this.mobileViewEnabled === 'undefined') {
7398 enableMobileView = windowWidth < theme.mediaQuerySmall;
7399 enableDesktopView = windowWidth >= theme.mediaQuerySmall;
7400 } else {
7401 enableMobileView =
7402 windowWidth < theme.mediaQuerySmall && !this.mobileViewEnabled;
7403 enableDesktopView =
7404 windowWidth >= theme.mediaQuerySmall && this.mobileViewEnabled;
7405 }
7406
7407 if (enableMobileView) {
7408 this.mobileViewEnabled = true;
7409 this._destroyDesktopState();
7410 this._enableMobileState();
7411 }
7412
7413 if (enableDesktopView) {
7414 this.mobileViewEnabled = false;
7415 this._destroyMobileState();
7416 this._enableDesktopState();
7417 }
7418 },
7419
7420 _enableDesktopState: function() {
7421 this.initDesktopSlideshow();
7422 },
7423
7424 _destroyDesktopState: function() {
7425 if (!this.desktopSlideshow) return;
7426
7427 this.destroyDesktopSlideshow();
7428 this._resetSlideshowValues();
7429 },
7430
7431 _enableMobileState: function() {
7432 this.initMobileSlideshow();
7433 },
7434
7435 _destroyMobileState: function() {
7436 if (!this.mobileSlideshow) return;
7437
7438 this.destroyMobileSlideshow();
7439 this._resetSlideshowValues();
7440 },
7441
7442 // This method is called when the viewport goes from mobile to desktop
7443 // and vice versa. It ensures the slideshow resets to the first slide,
7444 // which helps with potential conflicting values based on shared markup.
7445 _resetSlideshowValues: function() {
7446 $(selectors$24.slide)
7447 .removeClass(classes$22.slideActive)
7448 .first()
7449 .addClass(classes$22.slideActive);
7450
7451 $(selectors$24.button)
7452 .removeClass(classes$22.buttonActive)
7453 .first()
7454 .addClass(classes$22.buttonActive);
7455 },
7456
7457 _onFocusIn: function(evt) {
7458 if (
7459 this.$container.has(evt.target).length &&
7460 this.$slideshow.attr('aria-live') === 'polite'
7461 ) {
7462 return;
7463 }
7464
7465 this.$slideshow.attr('aria-live', 'polite');
7466 },
7467
7468 _onFocusOut: function(evt) {
7469 if (this.$container.has(evt.relatedTarget).length) {
7470 return;
7471 }
7472
7473 this.$slideshow.removeAttr('aria-live');
7474 }
7475});
7476
7477var selectors$27 = {
7478 loadPlayerButton: '.video-section__load-player-button',
7479 closePlayerButton: '.video-section__player-close',
7480 playerContainer: '.video-section__player',
7481 cover: '.video-section__cover',
7482 errorMessage: '.video-section__error',
7483 bodyOverlay: '.video-section__body-overlay'
7484};
7485
7486var classes$25 = {
7487 playerLoading: 'video-section--loading',
7488 playerLoaded: 'video-section--loaded',
7489 playerError: 'video-section--error',
7490 animateButton: 'animated pulse'
7491};
7492
7493sections.register('video-section', {
7494 onLoad: function() {
7495 this.$container = $(this.container);
7496
7497 this.$container
7498 .on('click', selectors$27.loadPlayerButton, this._loadPlayer.bind(this))
7499 .on('click', selectors$27.closePlayerButton, this._closePlayer.bind(this))
7500 .on('click', selectors$27.bodyOverlay, this._closePlayer.bind(this));
7501 },
7502
7503 _loadPlayer: function() {
7504 var $container = this.$container;
7505 var $loadButton = $(selectors$27.loadPlayerButton, $container);
7506 var $playerContainer = $(selectors$27.playerContainer, $container);
7507 var playerType = this.$container.attr('data-video-type');
7508 var promiseVideoPlayer;
7509
7510 $loadButton.addClass(classes$25.animateButton);
7511
7512 this._scrollToPlayer($container);
7513
7514 if (playerType === 'youtube') {
7515 promiseVideoPlayer = this._loadYoutubePlayer($playerContainer[0]);
7516 } else if (playerType === 'vimeo') {
7517 promiseVideoPlayer = this._loadVimeoPlayer($playerContainer[0]);
7518 }
7519
7520 return promiseVideoPlayer
7521 .then(this._onPlayerLoadReady.bind(this))
7522 .catch(this._onPlayerLoadError.bind(this))
7523 .always(function() {
7524 $loadButton.removeClass(classes$25.animateButton);
7525 });
7526 },
7527
7528 _scrollToPlayer: function(container) {
7529 var containerTop = container.offset().top;
7530 var offset = ($(window).height() - container.height()) / 2;
7531
7532 $('html, body').animate(
7533 {
7534 scrollTop: containerTop - offset
7535 },
7536 400
7537 );
7538 },
7539
7540 _loadYoutubePlayer: function(container) {
7541 return youtube
7542 .promisePlayer(container, {
7543 videoId: this.$container.attr('data-video-id'),
7544 ratio: 16 / 9,
7545 playerVars: {
7546 modestbranding: 1,
7547 autoplay: 1,
7548 showinfo: 0,
7549 rel: 0
7550 }
7551 })
7552 .then(
7553 function(player) {
7554 this.player = player;
7555 }.bind(this)
7556 );
7557 },
7558
7559 _loadVimeoPlayer: function(container) {
7560 return vimeo
7561 .promisePlayer(container, {
7562 id: this.$container.attr('data-video-id')
7563 })
7564 .then(
7565 function(player) {
7566 this.player = player;
7567 this.player.play();
7568 }.bind(this)
7569 );
7570 },
7571
7572 _onPlayerLoadReady: function() {
7573 $(selectors$27.closePlayerButton, this.$container)
7574 .show()
7575 .focus();
7576 $(selectors$27.cover, this.$container)
7577 .prepareTransition()
7578 .addClass(classes$25.playerLoaded);
7579 this.$container.addClass(classes$25.playerLoaded);
7580
7581 this._setScrollPositionValues();
7582
7583 $(document)
7584 .one('keyup' + this.namespace, this._closeOnEscape.bind(this))
7585 .on('scroll' + this.namespace, this._onScroll.bind(this));
7586
7587 $(window).on(
7588 'resize' + this.namespace,
7589 this._setScrollPositionValues.bind(this)
7590 );
7591 },
7592
7593 _onPlayerLoadError: function(err) {
7594 this.$container.addClass(classes$25.playerError);
7595 $(selectors$27.errorMessage, this.$container).text(err);
7596 },
7597
7598 _closeOnEscape: function(evt) {
7599 if (evt.keyCode !== 27) return;
7600
7601 this._closePlayer();
7602 $(selectors$27.loadPlayerButton, this.$container).focus();
7603 },
7604
7605 _onScroll: function() {
7606 var scrollTop = $(window).scrollTop();
7607
7608 if (
7609 scrollTop > this.videoTop + 0.25 * this.videoHeight ||
7610 scrollTop + this.windowHeight < this.videoBottom - 0.25 * this.videoHeight
7611 ) {
7612 // Debounce DOM edits to the next frame with requestAnimationFrame
7613 requestAnimationFrame(this._closePlayer.bind(this));
7614 }
7615 },
7616
7617 _setScrollPositionValues: function() {
7618 this.videoHeight = this.$container.outerHeight(true);
7619 this.videoTop = this.$container.offset().top;
7620 this.videoBottom = this.videoTop + this.videoHeight;
7621 this.windowHeight = $(window).innerHeight();
7622 },
7623
7624 _closePlayer: function() {
7625 $(selectors$27.cover, this.$container)
7626 .prepareTransition()
7627 .removeClass(classes$25.playerLoaded);
7628 this.$container.removeClass(classes$25.playerLoaded);
7629 $(selectors$27.closePlayerButton, this.$container).hide();
7630
7631 if (typeof this.player.destroy === 'function') {
7632 this.player.destroy();
7633 } else if (typeof this.player.unload === 'function') {
7634 this.player.unload();
7635 }
7636
7637 $(document).off(this.namespace);
7638 $(window).off(this.namespace);
7639 }
7640});
7641
7642// import templates.js and assign it to the global theme object
7643window.theme.templates = templates;
7644
7645// import all templates js
7646// import sections.js and assign it to the global theme object
7647window.theme.sections = sections;
7648
7649// import all sections js
7650$(document).ready(function() {
7651 templates.load('*');
7652 sections.load('*');
7653});
7654
7655}(jQuery,_.omit,_.find,_.throttle,Shopify.theme.a11y,_.remove,_.filter,_.fill,_.debounce,morphdom,_.defaultTo,_.findIndex,_.isArray,Hammer));
7656