PageSourceSearch

https://piece-piece.com/cdn/shop/t/8/assets/theme.min.js?v=83844523546999841661659282359

js piece-piece.com collected 2026-10-06 18:48:06 UTC 269,897 bytes, 8,369 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.