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"> ' + 2773 theme.strings.unitPriceSeparator + 2774 ' </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.