1window.theme = window.theme || {}; 2window.slate = window.slate || {}; 3 4window.lazySizesConfig = window.lazySizesConfig || {}; 5lazySizesConfig.expFactor = 4; 6 7(function($){ 8 var $ = jQuery = $; 9 10 slate.utils = { 11 /** 12 * _.defaultTo from lodash 13 * Checks `value` to determine whether a default value should be returned in 14 * its place. The `defaultValue` is returned if `value` is `NaN`, `null`, 15 * or `undefined`. 16 * Source: https://github.com/lodash/lodash/blob/master/defaultTo.js 17 * 18 * @param {*} value - Value to check 19 * @param {*} defaultValue - Default value 20 * @returns {*} - Returns the resolved value 21 */ 22 defaultTo: function(value, defaultValue) { 23 return (value == null || value !== value) ? defaultValue : value 24 } 25 }; 26 27 slate.a11y = { 28 29 /** 30 * Traps the focus in a particular container 31 * 32 * @param {object} options - Options to be used 33 * @param {jQuery} options.$container - Container to trap focus within 34 * @param {jQuery} options.$elementToFocus - Element to be focused when focus leaves container 35 * @param {string} options.namespace - Namespace used for new focus event handler 36 */ 37 trapFocus: function(options) { 38 var eventName = options.namespace 39 ? 'focusin.' + options.namespace 40 : 'focusin'; 41 42 if (!options.$elementToFocus) { 43 options.$elementToFocus = options.$container; 44 } 45 46 options.$container.attr('tabindex', '-1'); 47 options.$elementToFocus.focus(); 48 49 $(document).off('focusin'); 50 51 $(document).on(eventName, function(evt) { 52 if (options.$container[0] !== evt.target && !options.$container.has(evt.target).length) { 53 options.$container.focus(); 54 } 55 }); 56 }, 57 58 /** 59 * Removes the trap of focus in a particular container 60 * 61 * @param {object} options - Options to be used 62 * @param {jQuery} options.$container - Container to trap focus within 63 * @param {string} options.namespace - Namespace used for new focus event handler 64 */ 65 removeTrapFocus: function(options) { 66 var eventName = options.namespace 67 ? 'focusin.' + options.namespace 68 : 'focusin'; 69 70 if (options.$container && options.$container.length) { 71 options.$container.removeAttr('tabindex'); 72 } 73 74 $(document).off(eventName); 75 }, 76 77 78 // Not from Slate, but fit in the a11y category 79 lockMobileScrolling: function(namespace, $element) { 80 if ($element) { 81 var $el = $element; 82 } else { 83 var $el = $(document.documentElement).add('body'); 84 } 85 $el.on('touchmove' + namespace, function () { 86 return false; 87 }); 88 }, 89 90 unlockMobileScrolling: function(namespace, $element) { 91 if ($element) { 92 var $el = $element; 93 } else { 94 var $el = $(document.documentElement).add('body'); 95 } 96 $el.off(namespace); 97 } 98 }; 99 100 theme.Sections = function Sections() { 101 this.constructors = {}; 102 this.instances = []; 103 104 $(document) 105 .on('shopify:section:load', this._onSectionLoad.bind(this)) 106 .on('shopify:section:unload', this._onSectionUnload.bind(this)) 107 .on('shopify:section:select', this._onSelect.bind(this)) 108 .on('shopify:section:deselect', this._onDeselect.bind(this)) 109 .on('shopify:block:select', this._onBlockSelect.bind(this)) 110 .on('shopify:block:deselect', this._onBlockDeselect.bind(this)); 111 }; 112 113 theme.Sections.prototype = $.extend({}, theme.Sections.prototype, { 114 createInstance: function(container, constructor, customScope) { 115 var $container = $(container); 116 var id = $container.attr('data-section-id'); 117 var type = $container.attr('data-section-type'); 118 119 constructor = constructor || this.constructors[type]; 120 121 if (typeof constructor === 'undefined') { 122 return; 123 } 124 125 // If custom scope passed, check to see if instance 126 // is already initialized so we don't double up 127 if (customScope) { 128 var instanceExists = this._findInstance(id); 129 if (instanceExists) { 130 return; 131 } 132 } 133 134 var instance = $.extend(new constructor(container), {
135 id: id, 136 type: type, 137 container: container 138 }); 139 140 this.instances.push(instance); 141 }, 142 143 _onSectionLoad: function(evt, subSection, subSectionId) { 144 if (AOS) { 145 AOS.refreshHard(); 146 } 147 var container = subSection ? subSection : $('[data-section-id]', evt.target)[0]; 148 149 if (!container) { 150 return; 151 } 152 153 this.createInstance(container); 154 155 var instance = subSection ? subSectionId : this._findInstance(evt.detail.sectionId); 156 157 if (!subSection) { 158 this._loadSubSections(); 159 } 160 161 // Run JS only in case of the section being selected in the editor 162 // before merchant clicks "Add" 163 if (instance && typeof instance.onLoad === 'function') { 164 instance.onLoad(evt); 165 } 166 }, 167 168 _loadSubSections: function() { 169 if (AOS) { 170 AOS.refreshHard(); 171 } 172 $('[data-subsection]').each(function(evt, el) { 173 this._onSectionLoad(null, el, $(el).data('section-id')); 174 }.bind(this)); 175 }, 176 177 _onSectionUnload: function(evt) { 178 var instance = this._removeInstance(evt.detail.sectionId); 179 if (instance && typeof instance.onUnload === 'function') { 180 instance.onUnload(evt); 181 } 182 }, 183 184 _onSelect: function(evt) { 185 var instance = this._findInstance(evt.detail.sectionId); 186 187 if (instance && typeof instance.onSelect === 'function') { 188 instance.onSelect(evt); 189 } 190 }, 191 192 _onDeselect: function(evt) { 193 var instance = this._findInstance(evt.detail.sectionId); 194 195 if (instance && typeof instance.onDeselect === 'function') { 196 instance.onDeselect(evt); 197 } 198 }, 199 200 _onBlockSelect: function(evt) { 201 var instance = this._findInstance(evt.detail.sectionId); 202 203 if (instance && typeof instance.onBlockSelect === 'function') { 204 instance.onBlockSelect(evt); 205 } 206 }, 207 208 _onBlockDeselect: function(evt) { 209 var instance = this._findInstance(evt.detail.sectionId); 210 211 if (instance && typeof instance.onBlockDeselect === 'function') { 212 instance.onBlockDeselect(evt); 213 } 214 }, 215 216 _findInstance: function(id) { 217 for (var i = 0; i < this.instances.length; i++) { 218 if (this.instances[i].id === id) { 219 return this.instances[i]; 220 } 221 } 222 }, 223 224 _removeInstance: function(id) { 225 var i = this.instances.length; 226 var instance; 227 228 while(i--) { 229 if (this.instances[i].id === id) { 230 instance = this.instances[i]; 231 this.instances.splice(i, 1); 232 break; 233 } 234 } 235 236 return instance; 237 }, 238 239 register: function(type, constructor, $scope) { 240 var afterLoad = false; 241 this.constructors[type] = constructor; 242 var $sections = $('[data-section-type=' + type + ']'); 243 244 if ($scope) { 245 $sections = $('[data-section-type=' + type + ']', $scope); 246 } 247 248 $sections.each(function(index, container) { 249 this.createInstance(container, constructor, $scope); 250 }.bind(this)); 251 } 252 }); 253 254 255 /** 256 * Currency Helpers 257 * ----------------------------------------------------------------------------- 258 * A collection of useful functions that help with currency formatting 259 * 260 * Current contents 261 * - formatMoney - Takes an amount in cents and returns it as a formatted dollar value. 262 * 263 * Alternatives 264 * - Accounting.js - http://openexchangerates.github.io/accounting.js/ 265 * 266 */ 267 268 theme.Currency = (function() { 269 var moneyFormat = '$'; 270 271 function formatMoney(cents, format) { 272 if (typeof cents === 'string') { 273 cents = cents.replace('.', ''); 274 } 275 var value = ''; 276 var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/; 277 var formatString = (format || moneyFormat); 278 279 function formatWithDelimiters(number, precision, thousands, decimal) { 280 precision = slate.utils.defaultTo(precision, 2); 281 thousands = slate.utils.defaultTo(thousands, ','); 282 decimal = slate.utils.defaultTo(decimal, '.'); 283 284 if (isNaN(number) || number == null) { 285 return 0; 286 } 287 288 number = (number / 100.0).toFixed(precision); 289 290 var parts = number.split('.'); 291 var dollarsAmount = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + thousands); 292 var centsAmount = parts[1] ? (decimal + parts[1]) : ''; 293
294 return dollarsAmount + centsAmount; 295 } 296 297 switch (formatString.match(placeholderRegex)[1]) { 298 case 'amount': 299 value = formatWithDelimiters(cents, 2); 300 break; 301 case 'amount_no_decimals': 302 value = formatWithDelimiters(cents, 0); 303 break; 304 case 'amount_with_comma_separator': 305 value = formatWithDelimiters(cents, 2, '.', ','); 306 break; 307 case 'amount_no_decimals_with_comma_separator': 308 value = formatWithDelimiters(cents, 0, '.', ','); 309 break; 310 case 'amount_no_decimals_with_space_separator': 311 value = formatWithDelimiters(cents, 0, ' '); 312 break; 313 } 314 315 return formatString.replace(placeholderRegex, value); 316 } 317 318 return { 319 formatMoney: formatMoney 320 } 321 })(); 322 323 324 /** 325 * Image Helper Functions 326 * ----------------------------------------------------------------------------- 327 * A collection of functions that help with basic image operations. 328 * 329 */ 330 331 theme.Images = (function() { 332 333 /** 334 * Find the Shopify image attribute size 335 * 336 * @param {string} src 337 * @returns {null} 338 */ 339 function imageSize(src) { 340 if (!src) { 341 return '620x'; // default based on theme 342 } 343 344 var match = src.match(/.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\.@]/); 345 346 if (match !== null) { 347 return match[1]; 348 } else { 349 return null; 350 } 351 } 352 353 /** 354 * Adds a Shopify size attribute to a URL 355 * 356 * @param src 357 * @param size 358 * @returns {*} 359 */ 360 function getSizedImageUrl(src, size) { 361 if (size == null) { 362 return src; 363 } 364 365 if (size === 'master') { 366 return this.removeProtocol(src); 367 } 368 369 var match = src.match(/\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i); 370 371 if (match != null) { 372 var prefix = src.split(match[0]); 373 var suffix = match[0]; 374 375 return this.removeProtocol(prefix[0] + '_' + size + suffix); 376 } 377 378 return null; 379 } 380 381 function removeProtocol(path) { 382 return path.replace(/http(s)?:/, ''); 383 } 384 385 return { 386 imageSize: imageSize, 387 getSizedImageUrl: getSizedImageUrl, 388 removeProtocol: removeProtocol 389 }; 390 })(); 391 392 slate.Variants = (function() { 393 394 function Variants(options) { 395 this.$container = options.$container; 396 this.variants = options.variants; 397 this.singleOptionSelector = options.singleOptionSelector; 398 this.originalSelectorId = options.originalSelectorId; 399 this.enableHistoryState = options.enableHistoryState; 400 this.currentVariant = this._getVariantFromOptions(); 401 402 $(this.singleOptionSelector, this.$container).on('change', this._onSelectChange.bind(this)); 403 } 404 405 Variants.prototype = $.extend({}, Variants.prototype, { 406
407 _getCurrentOptions: function() { 408 var currentOptions = $.map($(this.singleOptionSelector, this.$container), function(element) { 409 var $element = $(element); 410 var type = $element.attr('type'); 411 var currentOption = {}; 412 413 if (type === 'radio' || type === 'checkbox') { 414 if ($element[0].checked) { 415 currentOption.value = $element.val(); 416 currentOption.index = $element.data('index'); 417 418 return currentOption; 419 } else { 420 return false; 421 } 422 } else { 423 currentOption.value = $element.val(); 424 currentOption.index = $element.data('index'); 425 426 return currentOption; 427 } 428 }); 429 430 // remove any unchecked input values if using radio buttons or checkboxes 431 currentOptions = this._compact(currentOptions); 432 433 return currentOptions; 434 }, 435 436 _getVariantFromOptions: function() { 437 var selectedValues = this._getCurrentOptions(); 438 var variants = this.variants; 439 var found = false; 440
441 variants.forEach(function(variant) { 442 var match = true; 443 var options = variant.options; 444 445 selectedValues.forEach(function(option) { 446 // console.log('try to match ' + option.value + ' with ' + variant[option.index]); 447 448 if (match) { 449 match = (variant[option.index] === option.value); 450 } 451 }); 452 453 if (match) { 454 found = variant; 455 } 456 }); 457 458 return found || null; 459 }, 460 461 _onSelectChange: function() { 462 var variant = this._getVariantFromOptions(); 463 464 this.$container.trigger({ 465 type: 'variantChange', 466 variant: variant 467 }); 468 469 if (!variant) { 470 return; 471 } 472 473 this._updateMasterSelect(variant); 474 this._updateImages(variant); 475 this._updatePrice(variant); 476 this._updateSKU(variant); 477 this.currentVariant = variant; 478 479 if (this.enableHistoryState) { 480 this._updateHistoryState(variant); 481 } 482 }, 483 484 _updateImages: function(variant) { 485 var variantImage = variant.featured_image || {}; 486 var currentVariantImage = this.currentVariant.featured_image || {}; 487 488 if (!variant.featured_image || variantImage.src === currentVariantImage.src) { 489 return; 490 } 491 492 this.$container.trigger({ 493 type: 'variantImageChange', 494 variant: variant 495 }); 496 }, 497 498 _updatePrice: function(variant) { 499 if (variant.price === this.currentVariant.price && variant.compare_at_price === this.currentVariant.compare_at_price) { 500 return; 501 } 502 503 this.$container.trigger({ 504 type: 'variantPriceChange', 505 variant: variant 506 }); 507 }, 508 509 _updateSKU: function(variant) { 510 if (variant.sku === this.currentVariant.sku) { 511 return; 512 } 513 514 this.$container.trigger({ 515 type: 'variantSKUChange', 516 variant: variant 517 }); 518 }, 519 520 _updateHistoryState: function(variant) { 521 if (!history.replaceState || !variant) { 522 return; 523 } 524 525 var newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?variant=' + variant.id; 526 window.history.propertyIsEnumerable({path: newurl}, '', newurl); 527 }, 528 529 _updateMasterSelect: function(variant) { 530 $(this.originalSelectorId, this.$container).val(variant.id); 531 }, 532 533 // _.compact from lodash 534 // https://github.com/lodash/lodash/blob/4d4e452ade1e78c7eb890968d851f837be37e429/compact.js 535 _compact: function(array) { 536 var index = -1, 537 length = array == null ? 0 : array.length, 538 resIndex = 0, 539 result = []; 540 541 while (++index < length) { 542 var value = array[index]; 543 if (value) { 544 result[resIndex++] = value; 545 } 546 } 547 return result; 548 } 549 }); 550 551 return Variants; 552 })(); 553 554 slate.rte = { 555 init: function() { 556 slate.rte.wrapTable(); 557 slate.rte.wrapVideo(); 558 slate.rte.imageLinks(); 559 }, 560 561 wrapTable: function() { 562 $('.rte table').wrap('<div class="table-wrapper"></div>'); 563 }, 564 565 wrapVideo: function() { 566 var $iframeVideo = $('.rte iframe[src*="youtube.com/embed"], .rte iframe[src*="player.vimeo"]'); 567 var $iframeReset = $iframeVideo.add('iframe#admin_bar_iframe'); 568 569 $iframeVideo.each(function () { 570 // Add wrapper to make video responsive 571 if (!$(this).parents('.video-wrapper').length) { 572 $(this).wrap('<div class="video-wrapper"></div>'); 573 } 574 }); 575 576 $iframeReset.each(function () { 577 // Re-set the src attribute on each iframe after page load 578 // for Chrome's "incorrect iFrame content on 'back'" bug. 579 // https://code.google.com/p/chromium/issues/detail?id=395791 580 // Need to specifically target video and admin bar 581 this.src = this.src; 582 }); 583 }, 584 585 // Remove CSS that adds animated underline under image links 586 imageLinks: function() { 587 $('.rte a img').parent().addClass('rte__image'); 588 } 589 }; 590 591 592 theme.Modals = (function() { 593 function Modal(id, name, options) { 594 var defaults = { 595 close: '.js-modal-close', 596 open: '.js-modal-open-' + name,
597 openClass: 'modal--is-active', 598 closingClass: 'modal--is-closing', 599 bodyOpenClass: 'modal-open', 600 bodyOpenSolidClass: 'modal-open--solid', 601 bodyClosingClass: 'modal-closing', 602 closeOffContentClick: true 603 }; 604 605 this.id = id; 606 this.$modal = $('#' + id); 607 608 if (!this.$modal.length) { 609 return false; 610 } 611 612 this.nodes = { 613 $parent: $('html').add('body'), 614 $modalContent: this.$modal.find('.modal__inner') 615 }; 616 617 this.config = $.extend(defaults, options); 618 this.modalIsOpen = false; 619 this.$focusOnOpen = this.config.focusOnOpen ? $(this.config.focusOnOpen) : this.$modal; 620 this.isSolid = this.config.solid; 621 622 this.init(); 623 } 624 625 Modal.prototype.init = function() { 626 var $openBtn = $(this.config.open); 627 628 // Add aria controls 629 $openBtn.attr('aria-expanded', 'false'); 630 631 $(this.config.open).on('click', this.open.bind(this)); 632 this.$modal.find(this.config.close).on('click', this.close.bind(this)); 633 634 // Close modal if a drawer is opened 635 $('body').on('drawerOpen', function() { 636 this.close(); 637 }.bind(this)); 638 }; 639 640 Modal.prototype.open = function(evt) { 641 // Keep track if modal was opened from a click, or called by another function 642 var externalCall = false; 643 644 // don't open an opened modal 645 if (this.modalIsOpen) { 646 return; 647 } 648 649 // Prevent following href if link is clicked 650 if (evt) { 651 evt.preventDefault(); 652 } else { 653 externalCall = true; 654 } 655 656 // Without this, the modal opens, the click event bubbles up to $nodes.page 657 // which closes the modal. 658 if (evt && evt.stopPropagation) { 659 evt.stopPropagation(); 660 // save the source of the click, we'll focus to this on close 661 this.$activeSource = $(evt.currentTarget); 662 } 663 664 if (this.modalIsOpen && !externalCall) { 665 this.close(); 666 } 667 668 this.$modal 669 .prepareTransition() 670 .addClass(this.config.openClass); 671 this.nodes.$parent.addClass(this.config.bodyOpenClass); 672 673 if (this.isSolid) { 674 this.nodes.$parent.addClass(this.config.bodyOpenSolidClass); 675 } 676 677 this.modalIsOpen = true; 678 679 slate.a11y.trapFocus({ 680 $container: this.$modal, 681 $elementToFocus: this.$focusOnOpen, 682 namespace: 'modal_focus' 683 }); 684 685 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { 686 this.$activeSource.attr('aria-expanded', 'true'); 687 } 688 689 $('body').trigger('modalOpen.' + this.id); 690 691 this.bindEvents(); 692 }; 693 694 Modal.prototype.close = function() { 695 // don't close a closed modal 696 if (!this.modalIsOpen) { 697 return; 698 } 699 700 // deselect any focused form elements 701 $(document.activeElement).trigger('blur'); 702 703 this.$modal 704 .prepareTransition() 705 .removeClass(this.config.openClass) 706 .addClass(this.config.closingClass); 707 this.nodes.$parent.removeClass(this.config.bodyOpenClass); 708 this.nodes.$parent.addClass(this.config.bodyClosingClass); 709 var o = this; 710 window.setTimeout(function() { 711 o.nodes.$parent.removeClass(o.config.bodyClosingClass); 712 o.$modal.removeClass(o.config.closingClass); 713 }, 500); // modal close css transition 714 715 if (this.isSolid) { 716 this.nodes.$parent.removeClass(this.config.bodyOpenSolidClass); 717 } 718 719 this.modalIsOpen = false; 720 721 slate.a11y.removeTrapFocus({ 722 $container: this.$modal, 723 namespace: 'modal_focus' 724 }); 725 726 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { 727 this.$activeSource.attr('aria-expanded', 'false').focus(); 728 } 729 730 $('body').trigger('modalClose.' + this.id); 731 732 this.unbindEvents(); 733 }; 734 735 Modal.prototype.bindEvents = function() { 736 // Pressing escape closes modal 737 this.nodes.$parent.on('keyup.modal', function(evt) { 738 if (evt.keyCode === 27) { 739 this.close(); 740 } 741 }.bind(this)); 742 743 if (this.config.closeOffContentClick) { 744 // Clicking outside of the modal content also closes it 745 this.$modal.on('click.modal', this.close.bind(this)); 746 747 // Exception to above: clicking anywhere on the modal content will NOT close it 748 this.nodes.$modalContent.on('click.modal', function(evt) { 749 evt.stopImmediatePropagation(); 750 }); 751 } 752 }; 753 754 Modal.prototype.unbindEvents = function() { 755 this.nodes.$parent.off('.modal'); 756 757 if (this.config.closeOffContentClick) { 758 this.$modal.off('.modal'); 759 this.nodes.$modalContent.off('.modal'); 760 } 761 }; 762 763 return Modal; 764 })(); 765 766 theme.Drawers = (function() { 767 function Drawer(id, name) { 768 this.config = { 769 id: id, 770 close: '.js-drawer-close', 771 open: '.js-drawer-open-' + name, 772 openClass: 'js-drawer-open', 773 closingClass: 'js-drawer-closing', 774 activeDrawer: 'drawer--is-open', 775 namespace: '.drawer-' + name 776 }; 777 778 this.$nodes = { 779 parent: $(document.documentElement).add('body'), 780 page: $('#MainContent') 781 }; 782 783 this.$drawer = $('#' + id); 784 785 if (!this.$drawer.length) { 786 return false;
787 } 788 789 this.isOpen = false; 790 this.init(); 791 }; 792 793 Drawer.prototype = $.extend({}, Drawer.prototype, { 794 init: function() { 795 var $openBtn = $(this.config.open); 796 797 // Add aria controls 798 $openBtn.attr('aria-expanded', 'false'); 799 800 $openBtn.on('click', this.open.bind(this)); 801 this.$drawer.find(this.config.close).on('click', this.close.bind(this)); 802 }, 803 804 open: function(evt) { 805 if (evt) { 806 evt.preventDefault(); 807 } 808 809 if (this.isOpen) { 810 return; 811 } 812 813 // Without this the drawer opens, the click event bubbles up to $nodes.page which closes the drawer. 814 if (evt && evt.stopPropagation) { 815 evt.stopPropagation(); 816 // save the source of the click, we'll focus to this on close 817 this.$activeSource = $(evt.currentTarget); 818 } 819 820 this.$drawer.prepareTransition().addClass(this.config.activeDrawer); 821 822 this.$nodes.parent.addClass(this.config.openClass); 823 this.isOpen = true; 824 825 slate.a11y.trapFocus({ 826 $container: this.$drawer, 827 namespace: 'drawer_focus' 828 }); 829 830 $('body').trigger('drawerOpen.' + this.config.id); 831 832 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { 833 this.$activeSource.attr('aria-expanded', 'true'); 834 } 835 836 this.bindEvents(); 837 }, 838 839 close: function() { 840 if (!this.isOpen) { 841 return; 842 } 843 844 // deselect any focused form elements 845 $(document.activeElement).trigger('blur'); 846 847 this.$drawer.prepareTransition().removeClass(this.config.activeDrawer); 848 849 this.$nodes.parent.removeClass(this.config.openClass); 850 this.$nodes.parent.addClass(this.config.closingClass); 851 var o = this; 852 window.setTimeout(function() { 853 o.$nodes.parent.removeClass(o.config.closingClass); 854 }, 500); 855 856 this.isOpen = false; 857 858 slate.a11y.removeTrapFocus({ 859 $container: this.$drawer, 860 namespace: 'drawer_focus' 861 }); 862 863 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { 864 this.$activeSource.attr('aria-expanded', 'false'); 865 } 866 867 this.unbindEvents(); 868 }, 869 870 bindEvents: function() { 871 slate.a11y.lockMobileScrolling(this.config.namespace, this.$nodes.page); 872 873 // Clicking out of drawer closes it 874 this.$nodes.page.on('click' + this.config.namespace, function () { 875 this.close(); 876 return false; 877 }.bind(this)); 878 879 // Pressing escape closes drawer 880 this.$nodes.parent.on('keyup' + this.config.namespace, function(evt) { 881 if (evt.keyCode === 27) { 882 this.close(); 883 } 884 }.bind(this)); 885 }, 886 887 unbindEvents: function() { 888 slate.a11y.unlockMobileScrolling(this.config.namespace, this.$nodes.page); 889 this.$nodes.parent.off(this.config.namespace); 890 this.$nodes.page.off(this.config.namespace); 891 } 892 }); 893 894 return Drawer; 895 })(); 896 897 theme.cart = { 898 getCart: function() { 899 return $.getJSON('/cart.js'); 900 }, 901 902 changeItem: function(key, qty) { 903 return this._updateCart({ 904 type: 'POST', 905 url: '/cart/change.js', 906 data: 'quantity=' + qty + '&id=' + key, 907 dataType: 'json' 908 }); 909 }, 910 911 addItemFromForm: function(data) { 912 return this._updateCart({ 913 type: 'POST', 914 url: '/cart/add.js', 915 data: data, 916 dataType: 'json' 917 }); 918 }, 919 920 _updateCart: function(params) { 921 return $.ajax(params) 922 .then(function(cart) { 923 return cart; 924 }.bind(this)) 925 }, 926 927 updateNote: function(note) { 928 var params = { 929 type: 'POST', 930 url: '/cart/update.js', 931 data: 'note=' + theme.cart.attributeToString(note), 932 dataType: 'json', 933 success: function(cart) {}, 934 error: function(XMLHttpRequest, textStatus) {} 935 }; 936 937 $.ajax(params); 938 }, 939 940 attributeToString: function(attribute) { 941 if ((typeof attribute) !== 'string') { 942 attribute += ''; 943 if (attribute === 'undefined') { 944 attribute = ''; 945 } 946 } 947 return $.trim(attribute); 948 } 949 } 950 951 $(function() { 952 // Add a loading indicator on the cart checkout button (/cart and drawer) 953 $('body').on('click', '.cart__checkout', function() { 954 $(this).addClass('btn--loading'); 955 }); 956 957 $('body').on('change', 'textarea[name="note"]', function() { 958 var newNote = $(this).val(); 959 theme.cart.updateNote(newNote); 960 }); 961 962 963 // Custom JS to prevent checkout without confirming terms and conditions 964 $('body').on('click', '.cart__checkout--ajax', function(evt) { 965 if ($('#CartAgree').is(':checked')) { 966 } else { 967 alert(theme.strings.cartTermsConfirmation); 968 $(this).removeClass('btn--loading'); 969 return false;
970 } 971 }); 972 973 $('body').on('click', '.cart__checkout--page', function(evt) { 974 if ($('#CartPageAgree').is(':checked')) { 975 } else { 976 alert(theme.strings.cartTermsConfirmation); 977 $(this).removeClass('btn--loading'); 978 return false; 979 } 980 }); 981 }); 982 983 theme.QtySelector = (function() { 984 var classes = { 985 input: '.js-qty__num', 986 plus: '.js-qty__adjust--plus', 987 minus: '.js-qty__adjust--minus' 988 }; 989 990 function QtySelector($el, options) { 991 this.$wrapper = $el; 992 this.$input = $el.find(classes.input); 993 this.$plus = $el.find(classes.plus); 994 this.$minus = $el.find(classes.minus); 995 this.minValue = this.$input.attr('min') || 1; 996 997 var defaults = { 998 namespace: null, 999 key: this.$input.data('id') 1000 }; 1001 1002 this.options = $.extend(defaults, options); 1003 1004 this.initEventListeners(); 1005 }; 1006 1007 QtySelector.prototype = $.extend({}, QtySelector.prototype, { 1008 initEventListeners: function() { 1009 this.$plus.on('click', function() { 1010 var qty = this.validateQty(this.$input.val()); 1011 this.addQty(qty); 1012 }.bind(this)); 1013 1014 this.$minus.on('click', function() { 1015 var qty = this.validateQty(this.$input.val()); 1016 this.subtractQty(qty); 1017 }.bind(this)); 1018 1019 this.$input.on('change', function() { 1020 var qty = this.validateQty(this.$input.val()); 1021 this.changeQty(qty); 1022 }.bind(this)); 1023 }, 1024 1025 addQty: function(number) { 1026 var qty = number + 1; 1027 this.changeQty(qty); 1028 }, 1029 1030 subtractQty: function(number) { 1031 var qty = number - 1; 1032 if (qty <= this.minValue) { 1033 qty = this.minValue; 1034 } 1035 this.changeQty(qty); 1036 }, 1037 1038 changeQty: function(qty) { 1039 this.$input.val(qty); 1040 $('body').trigger('qty' + this.options.namespace, [this.options.key, qty]); 1041 }, 1042 1043 validateQty: function(number) { 1044 if((parseFloat(number) == parseInt(number)) && !isNaN(number)) { 1045 // We have a valid number! 1046 } else { 1047 // Not a number. Default to 1. 1048 number = 1; 1049 } 1050 return parseInt(number); 1051 } 1052 }); 1053 1054 return QtySelector; 1055 })(); 1056 1057 theme.CartDrawer = (function() { 1058 var config = { 1059 namespace: '.ajaxcart' 1060 }; 1061 1062 var selectors = { 1063 drawer: '#CartDrawer', 1064 container: '#CartContainer', 1065 template: '#CartTemplate', 1066 fixedFooter: '.drawer__footer--fixed', 1067 fixedInnerContent: '.drawer__inner--has-fixed-footer', 1068 cartBubble: '.cart-link__bubble' 1069 }; 1070 1071 function CartDrawer() { 1072 this.status = { 1073 loaded: false, 1074 loading: false 1075 }; 1076 1077 this.drawer = new theme.Drawers('CartDrawer', 'cart'); 1078 1079 // Prep handlebars template 1080 var source = $(selectors.template).html(); 1081 this.template = Handlebars.compile(source); 1082 1083 // Build cart on page load so it's ready in the drawer 1084 theme.cart.getCart().then(this.buildCart.bind(this)); 1085 1086 this.initEventListeners(); 1087 }; 1088 1089 CartDrawer.prototype = $.extend({}, CartDrawer.prototype, { 1090 initEventListeners: function() { 1091 $('body').on('updateCart' + config.namespace, this.initQtySelectors.bind(this)); 1092 $('body').on('updateCart' + config.namespace, this.sizeFooter.bind(this)); 1093 $('body').on('updateCart' + config.namespace, this.updateCartNotification.bind(this)); 1094 $('body').on('drawerOpen.CartDrawer', this.sizeFooter.bind(this)); 1095 1096 $(window).on('resize' + config.namespace, $.debounce(150, this.sizeFooter.bind(this))); 1097 1098 $('body').on('added.ajaxProduct', function() { 1099 theme.cart.getCart().then(function(cart) { 1100 this.buildCart(cart, true); 1101 }.bind(this)); 1102 }.bind(this)); 1103 }, 1104 1105 buildCart: function(cart, openDrawer) { 1106 this.loading(true); 1107 this.emptyCart(); 1108 1109 if (cart.item_count === 0) { 1110 $(selectors.container).append('<p class="appear-animation appear-delay-3">' + theme.strings.cartEmpty +'</p>'); 1111 } else { 1112 var items = []; 1113 var item = {}; 1114 var data = {}; 1115 var animation_row = 1; 1116 1117 $.each(cart.items, function(index, product) { 1118 1119 var prodImg; 1120 if (product.image !== null) { 1121 prodImg = product.image.replace(/(\.[^.]*)$/, "_180x$1"); 1122 } else { 1123 prodImg = 's/assets/admin/no-image-medium-cc9732cb976dd349a0df1d39816fbcc7.gif'; 1124 } 1125 1126 if (product.properties !== null) { 1127 $.each(product.properties, function(key, value) { 1128 if (key.charAt(0) === '_' || !value) { 1129 delete product.properties[key]; 1130 } 1131 }); 1132 } 1133 1134 // If we have line-item discount, add formattedAmount to discount object 1135 var amount = 0; 1136 if (product.line_level_discount_allocations.length !== 0) { 1137 for (var discount in product.line_level_discount_allocations) { 1138 amount = product.line_level_discount_allocations[discount].amount; 1139 1140 product.line_level_discount_allocations[discount].formattedAmount = theme.Currency.formatMoney(amount, theme.settings.moneyFormat); 1141 } 1142 } 1143 1144 animation_row+=2; 1145 1146 item = { 1147 key: product.key, 1148 url: product.url, 1149 img: prodImg, 1150 animationRow: animation_row, 1151 name: product.product_title, 1152 variation: product.variant_title, 1153 properties: product.properties, 1154 itemQty: product.quantity, 1155 price: theme.Currency.formatMoney(product.price, theme.settings.moneyFormat), 1156 discountedPrice: theme.Currency.formatMoney((product.price - (product.total_discount/product.quantity)), theme.settings.moneyFormat), 1157 discounts: product.line_level_discount_allocations, 1158 discountsApplied: product.line_level_discount_allocations.length === 0 ? false : true, 1159 vendor: product.vendor 1160 }; 1161 1162 items.push(item); 1163 }); 1164 1165 animation_row+=2; 1166 1167 // If we have cart discount, add a formattedAmount to the discount object 1168 var cartAmount = 0; 1169 if (cart.cart_level_discount_applications.length !== 0) { 1170 for (var cartDiscount in cart.cart_level_discount_applications) { 1171 cartAmount = cart.cart_level_discount_applications[cartDiscount].total_allocated_amount; 1172 1173 cart.cart_level_discount_applications[cartDiscount].formattedAmount = theme.Currency.formatMoney(cartAmount,theme.settings.moneyFormat); 1174 } 1175 } 1176 1177 data = { 1178 items: items, 1179 note: cart.note, 1180 lastAnimationRow: animation_row, 1181 cartDiscounts: cart.cart_level_discount_applications, 1182 cartDiscountsApplied: cart.cart_level_discount_applications.length === 0 ? false : true, 1183 totalPrice: theme.Currency.formatMoney(cart.total_price, theme.settings.moneyFormat) 1184 }; 1185 1186 $(selectors.container).append(this.template(data)); 1187 } 1188 1189 this.status.loaded = true; 1190 this.loading(false); 1191 1192 if (theme.settings.currenciesEnabled) { 1193 theme.currencySwitcher.ajaxrefresh(); 1194 } 1195 1196 $('body').trigger('updateCart' + config.namespace, cart); 1197 1198 if (window.Shopify && Shopify.StorefrontExpressButtons) { 1199 Shopify.StorefrontExpressButtons.initialize(); 1200 1201 // Resize footer after arbitrary delay for buttons to load 1202 setTimeout(function() { 1203 this.sizeFooter(); 1204 }.bind(this), 800); 1205 } 1206 1207 // If specifically asked, open the cart drawer (only happens after product added from form) 1208 if (openDrawer === true) { 1209 this.drawer.open(); 1210 } 1211 }, 1212 1213 initQtySelectors: function() { 1214 $(selectors.container).find('.js-qty__wrapper').each(function(index, el) { 1215 var selector = new theme.QtySelector($(el), { 1216 namespace: '.cart-drawer' 1217 }); 1218 }.bind(this)); 1219 1220 $('body').on('qty.cart-drawer', this.updateItem.bind(this)); 1221 }, 1222
1223 updateItem: function(evt, key, qty) { 1224 if (this.status.loading) { 1225 return; 1226 } 1227 1228 this.loading(true); 1229 1230 theme.cart.changeItem(key, qty) 1231 .then(function(cart) { 1232 this.updateSuccess(cart); 1233 }.bind(this)) 1234 .catch(function(XMLHttpRequest) { 1235 this.updateError(XMLHttpRequest) 1236 }.bind(this)) 1237 .always(function() { 1238 this.loading(false); 1239 }.bind(this)); 1240 }, 1241 1242 loading: function(state) { 1243 this.status.loading = state; 1244 1245 if (state) { 1246 $(selectors.container).addClass('is-loading'); 1247 } else { 1248 $(selectors.container).removeClass('is-loading'); 1249 } 1250 }, 1251 1252 emptyCart: function() { 1253 $(selectors.container).empty(); 1254 }, 1255 1256 updateSuccess: function(cart) { 1257 this.buildCart(cart) 1258 }, 1259 1260 updateError: function(XMLHttpRequest) { 1261 if (XMLHttpRequest.responseJSON && XMLHttpRequest.responseJSON.description) { 1262 console.warn(XMLHttpRequest.responseJSON.description); 1263 } 1264 }, 1265 1266 // Update elements after cart is updated 1267 sizeFooter: function() { 1268 // Stop if our drawer doesn't have a fixed footer 1269 if (!$(selectors.drawer).hasClass('drawer--has-fixed-footer')) { 1270 return; 1271 } 1272 1273 // Elements are reprinted regularly so selectors are not cached 1274 var $cartFooter = $(selectors.drawer).find(selectors.fixedFooter).removeAttr('style'); 1275 var $cartInner = $(selectors.drawer).find(selectors.fixedInnerContent).removeAttr('style'); 1276 var cartFooterHeight = $cartFooter.outerHeight(); 1277 1278 $cartInner.css('bottom', cartFooterHeight); 1279 $cartFooter.css('height', cartFooterHeight); 1280 }, 1281 1282 updateCartNotification: function(evt, cart) { 1283 if (cart.items.length > 0) { 1284 $(selectors.cartBubble).addClass('cart-link__bubble--visible'); 1285 } else { 1286 $(selectors.cartBubble).removeClass('cart-link__bubble--visible'); 1287 } 1288 } 1289 }); 1290 1291 return CartDrawer; 1292 })(); 1293 1294 theme.AjaxProduct = (function() { 1295 var status = { 1296 loading: false 1297 }; 1298 1299 function ProductForm($form) { 1300 this.$form = $form; 1301 this.$addToCart = this.$form.find('.add-to-cart'); 1302 1303 if (this.$form.length) { 1304 this.$form.on('submit', this.addItemFromForm.bind(this)); 1305 } 1306 }; 1307 1308 ProductForm.prototype = $.extend({}, ProductForm.prototype, { 1309 addItemFromForm: function(evt, callback){ 1310 evt.preventDefault(); 1311 1312 if (status.loading) { 1313 console.warn('already adding item to cart | bail'); 1314 return; 1315 } 1316 1317 // Loading indicator on add to cart button 1318 this.$addToCart.addClass('btn--loading'); 1319 1320 status.loading = true; 1321 1322 var data = this.$form.serialize(); 1323 1324 theme.cart.addItemFromForm(data) 1325 .then(function(product) { 1326 this.success(product); 1327 }.bind(this)) 1328 .catch(function(XMLHttpRequest) { 1329 this.error(XMLHttpRequest) 1330 }.bind(this)) 1331 .always(function() { 1332 status.loading = false; 1333 this.$addToCart.removeClass('btn--loading'); 1334 }.bind(this)); 1335 }, 1336 1337 success: function(product) { 1338 this.$form.find('.errors').remove(); 1339 $('body').trigger('added.ajaxProduct'); 1340 }, 1341 1342 error: function(XMLHttpRequest) { 1343 this.$form.find('.errors').remove(); 1344 1345 if (XMLHttpRequest.responseJSON && XMLHttpRequest.responseJSON.description) { 1346 console.warn(XMLHttpRequest.responseJSON.description); 1347 1348 this.$form.prepend('<div class="errors text-center">' + XMLHttpRequest.responseJSON.description + '</div>'); 1349 } 1350 } 1351 }); 1352 1353 return ProductForm; 1354 })(); 1355 1356 theme.collapsibles = (function() { 1357 1358 var selectors = { 1359 trigger: '.collapsible-trigger', 1360 module: '.collapsible-content', 1361 moduleInner: '.collapsible-content__inner' 1362 }; 1363 1364 var classes = { 1365 hide: 'hide', 1366 open: 'is-open', 1367 autoHeight: 'collapsible--auto-height' 1368 }; 1369 1370 var namespace = '.collapsible'; 1371 1372 var isTransitioning = false;
1373 1374 function init() { 1375 $(selectors.trigger).each(function() { 1376 var $el = $(this); 1377 var state = $el.hasClass(classes.open); 1378 $el.attr('aria-expanded', state); 1379 }); 1380 1381 // Event listeners (hack for modals) 1382 $('body, .modal__inner') 1383 .off(namespace) 1384 .on('click' + namespace, selectors.trigger, function() { 1385 if (isTransitioning) { 1386 return; 1387 } 1388 1389 isTransitioning = true; 1390 1391 var $el = $(this); 1392 var isOpen = $el.hasClass(classes.open); 1393 var moduleId = $el.attr('aria-controls'); 1394 var $module = $('#' + moduleId); 1395 var height = $module.find(selectors.moduleInner).outerHeight(); 1396 var isAutoHeight = $el.hasClass(classes.autoHeight); 1397 1398 // If isAutoHeight, set the height to 0 just after setting the actual height 1399 // so the closing animation works nicely 1400 if (isOpen && isAutoHeight) { 1401 setTimeout(function() { 1402 height = 0; 1403 setTransitionHeight($module, height, isOpen, isAutoHeight); 1404 }, 0); 1405 } 1406 1407 if (isOpen && !isAutoHeight) { 1408 height = 0; 1409 } 1410 1411 $el 1412 .attr('aria-expanded', !isOpen) 1413 .toggleClass(classes.open, !isOpen); 1414 1415 setTransitionHeight($module, height, isOpen, isAutoHeight); 1416 }); 1417 } 1418 1419 function setTransitionHeight($module, height, isOpen, isAutoHeight) { 1420 $module 1421 .removeClass(classes.hide) 1422 .prepareTransition() 1423 .css('height', height) 1424 .toggleClass(classes.open, !isOpen); 1425 1426 if (!isOpen && isAutoHeight) { 1427 var o = $module; 1428 window.setTimeout(function() { 1429 o.css('height','auto'); 1430 isTransitioning = false; 1431 }, 500); 1432 } else { 1433 isTransitioning = false; 1434 } 1435 } 1436 1437 return { 1438 init: init 1439 }; 1440 })(); 1441 1442 theme.headerNav = (function() { 1443 1444 var $parent = $(document.documentElement).add('body'); 1445 var $page = $('#MainContent'); 1446 var selectors = { 1447 wrapper: '.header-wrapper', 1448 siteHeader: '.site-header', 1449 searchBtn: '.js-search-header', 1450 closeSearch: '.js-search-header-close', 1451 searchContainer: '.site-header__search-container', 1452 logoContainer: '.site-header__logo', 1453 logo: '.site-header__logo img', 1454 navigation: '.site-navigation', 1455 navContainerWithLogo: '.header-item--logo', 1456 navItems: '.site-nav__item', 1457 navLinks: '.site-nav__link', 1458 navLinksWithDropdown: '.site-nav__link--has-dropdown', 1459 navDropdownLinks: '.site-nav__dropdown-link--second-level' 1460 }; 1461 1462 var classes = { 1463 hasDropdownClass: 'site-nav--has-dropdown', 1464 hasSubDropdownClass: 'site-nav__deep-dropdown-trigger', 1465 dropdownActive: 'is-focused' 1466 }; 1467 1468 var config = { 1469 namespace: '.siteNav', 1470 wrapperOverlayed: false, 1471 overlayedClass: 'is-light', 1472 overlayEnabledClass: 'header-wrapper--overlay', 1473 stickyEnabled: false, 1474 stickyActive: false, 1475 stickyClass: 'site-header--stuck', 1476 openTransitionClass: 'site-header--opening', 1477 lastScroll: 0 1478 }; 1479 1480 // Elements used in resize functions, defined in init 1481 var $window; 1482 var $navContainerWithLogo; 1483 var $logoContainer; 1484 var $nav; 1485 var $wrapper; 1486 var $siteHeader; 1487 1488 function init() { 1489 $window = $(window); 1490 $navContainerWithLogo = $(selectors.navContainerWithLogo); 1491 $logoContainer = $(selectors.logoContainer); 1492 $nav = $(selectors.navigation); 1493 $wrapper = $(selectors.wrapper); 1494 $siteHeader = $(selectors.siteHeader); 1495 1496 config.stickyEnabled = $siteHeader.data('sticky'); 1497 if (config.stickyEnabled) { 1498 config.wrapperOverlayed = $wrapper.hasClass(config.overlayedClass); 1499 stickyHeader(); 1500 } 1501 1502 theme.settings.overlayHeader = $siteHeader.data('overlay'); 1503 // Disable overlay header if on collection template with no collection image 1504 if (theme.settings.overlayHeader && Shopify.designMode) { 1505 if ($('body').hasClass('template-collection') && !$('.collection-hero').length) { 1506 this.disableOverlayHeader(); 1507 } 1508 } 1509 1510 accessibleDropdowns(); 1511 searchDrawer(); 1512 1513 $window.on('load' + config.namespace, resizeLogo); 1514 $window.on('resize' + config.namespace, $.debounce(150, resizeLogo)); 1515 } 1516 1517 function unload() { 1518 $(window).off(config.namespace); 1519 $(selectors.searchBtn).off(config.namespace); 1520 $(selectors.closeSearch).off(config.namespace); 1521 $parent.off(config.namespace); 1522 $(selectors.navLinks).off(config.namespace); 1523 $(selectors.navDropdownLinks).off(config.namespace); 1524 } 1525 1526 function searchDrawer() {
1527 $(selectors.searchBtn).on('click' + config.namespace, function(evt) { 1528 evt.preventDefault(); 1529 openSearchDrawer(); 1530 }); 1531 1532 $(selectors.closeSearch).on('click' + config.namespace, function() { 1533 closeSearchDrawer(); 1534 }); 1535 } 1536 1537 function openSearchDrawer() { 1538 $(selectors.searchContainer).addClass('is-active'); 1539 $parent.addClass('js-drawer-open js-drawer-open--search'); 1540 1541 slate.a11y.trapFocus({ 1542 $container: $(selectors.searchContainer), 1543 namespace: 'header_search', 1544 $elementToFocus: $(selectors.searchContainer).find('input') 1545 }); 1546 1547 // If sticky is enabled, scroll to top on mobile when close to it 1548 // so you don't get an invisible search box 1549 if (theme.config.bpSmall && config.stickyEnabled && config.lastScroll < 300) { 1550 window.scrollTo(0,0); 1551 } 1552 1553 // Bind events 1554 slate.a11y.lockMobileScrolling(config.namespace); 1555 1556 // Clicking out of container closes it 1557 $page.on('click' + config.namespace, function () { 1558 closeSearchDrawer(); 1559 return false; 1560 }); 1561 1562 $parent.on('keyup' + config.namespace, function(evt) { 1563 if (evt.keyCode === 27) { 1564 closeSearchDrawer(); 1565 } 1566 }); 1567 } 1568 1569 function closeSearchDrawer() { 1570 // deselect any focused form elements 1571 $(document.activeElement).trigger('blur'); 1572 1573 $parent.removeClass('js-drawer-open js-drawer-open--search').off(config.namespace); 1574 $(selectors.searchContainer).removeClass('is-active'); 1575 1576 slate.a11y.removeTrapFocus({ 1577 $container: $(selectors.searchContainer), 1578 namespace: 'header_search' 1579 }); 1580 1581 slate.a11y.unlockMobileScrolling(config.namespace); 1582 $page.off('click' + config.namespace); 1583 $parent.off('keyup' + config.namespace); 1584 } 1585 1586 function resizeLogo() { 1587 // Using .each() as there can be a reversed logo too 1588 $(selectors.logo).each(function() { 1589 var $el = $(this), 1590 logoWidthOnScreen = $el.width(), 1591 containerWidth = $el.closest('.grid__item').width(); 1592 // If image exceeds container, let's make it smaller 1593 if (logoWidthOnScreen > containerWidth) { 1594 $el.css('maxWidth', containerWidth); 1595 } 1596 else { 1597 $el.removeAttr('style'); 1598 } 1599 }); 1600 } 1601 1602 function accessibleDropdowns() { 1603 var hasActiveDropdown = false; 1604 var hasActiveSubDropdown = false; 1605 var closeOnClickActive = false; 1606 1607 // Touch devices open dropdown on first click, navigate to link on second 1608 if (theme.config.isTouch) { 1609 $(selectors.navLinksWithDropdown).on('touchend' + config.namespace, function(evt) { 1610 var $el = $(this); 1611 var $parentItem = $el.parent();
1612 if (!$parentItem.hasClass(classes.dropdownActive)) { 1613 evt.preventDefault(); 1614 closeDropdowns(); 1615 openFirstLevelDropdown($el); 1616 } else { 1617 window.location.replace($el.attr('href')); 1618 } 1619 }); 1620 1621 $(selectors.navDropdownLinks).on('touchend' + config.namespace, function(evt) { 1622 var $el = $(this); 1623 var $parentItem = $el.parent(); 1624 1625 // Open third level menu or go to link based on active state 1626 if ($parentItem.hasClass(classes.hasSubDropdownClass)) { 1627 if (!$parentItem.hasClass(classes.dropdownActive)) { 1628 evt.preventDefault(); 1629 closeThirdLevelDropdown(); 1630 openSecondLevelDropdown($el); 1631 } else { 1632 window.location.replace($el.attr('href')); 1633 } 1634 } else { 1635 // No third level nav, go to link 1636 window.location.replace($el.attr('href')); 1637 } 1638 }); 1639 } 1640 1641 // Open/hide top level dropdowns 1642 $(selectors.navLinks).on('focusin mouseover' + config.namespace, function() { 1643 if (hasActiveDropdown) { 1644 closeSecondLevelDropdown(); 1645 } 1646 1647 if (hasActiveSubDropdown) { 1648 closeThirdLevelDropdown(); 1649 } 1650 1651 openFirstLevelDropdown($(this)); 1652 }); 1653 1654 // Force remove focus on sitenav links because focus sometimes gets stuck 1655 $(selectors.navLinks).on('mouseleave' + config.namespace, function() { 1656 closeDropdowns(); 1657 }); 1658 1659 // Open/hide sub level dropdowns 1660 $(selectors.navDropdownLinks).on('focusin' + config.namespace, function() { 1661 closeThirdLevelDropdown(); 1662 openSecondLevelDropdown($(this), true); 1663 }); 1664 1665 // Private dropdown methods 1666 function openFirstLevelDropdown($el) { 1667 var $parentItem = $el.parent();
1668 if ($parentItem.hasClass(classes.hasDropdownClass)) { 1669 $parentItem.addClass(classes.dropdownActive); 1670 hasActiveDropdown = true; 1671 } 1672 1673 if (!theme.config.isTouch) { 1674 if (!closeOnClickActive) { 1675 var eventType = theme.config.isTouch ? 'touchend' : 'click'; 1676 closeOnClickActive = true; 1677 $('body').on(eventType + config.namespace, function() { 1678 closeDropdowns(); 1679 $('body').off(config.namespace); 1680 closeOnClickActive = false; 1681 }); 1682 } 1683 } 1684 } 1685 1686 function openSecondLevelDropdown($el, skipCheck) { 1687 var $parentItem = $el.parent(); 1688 if ($parentItem.hasClass(classes.hasSubDropdownClass) || skipCheck) { 1689 $parentItem.addClass(classes.dropdownActive); 1690 hasActiveSubDropdown = true; 1691 } 1692 } 1693 1694 function closeDropdowns() { 1695 closeSecondLevelDropdown(); 1696 closeThirdLevelDropdown(); 1697 } 1698 1699 function closeSecondLevelDropdown() { 1700 $(selectors.navItems).removeClass(classes.dropdownActive); 1701 } 1702 1703 function closeThirdLevelDropdown() { 1704 $(selectors.navDropdownLinks).parent().removeClass(classes.dropdownActive); 1705 } 1706 } 1707 1708 function stickyHeader() { 1709 config.lastScroll = 0; 1710 $siteHeader.wrap('<div class="site-header-sticky"></div>'); 1711 1712 stickyHeaderHeight(); 1713 $window.on('resize' + config.namespace, $.debounce(50, stickyHeaderHeight)); 1714 $window.on('scroll' + config.namespace, $.throttle(15, stickyHeaderScroll)); 1715 1716 // This gets messed up in the editor, so here's a fix 1717 if (Shopify && Shopify.designMode) { 1718 setTimeout(function() { 1719 stickyHeaderHeight(); 1720 }, 250); 1721 } 1722 } 1723 1724 function stickyHeaderHeight() { 1725 var $stickyHeader = $('.site-header-sticky').css('height', $siteHeader.outerHeight(true)); 1726 } 1727 1728 function stickyHeaderScroll() { 1729 var scroll = $window.scrollTop(); 1730 var threshold = 250; 1731 1732 if (scroll > threshold) { 1733 if (config.stickyActive) { 1734 return; 1735 } 1736 1737 config.stickyActive = true; 1738 1739 $siteHeader.addClass(config.stickyClass); 1740 if (config.wrapperOverlayed) { 1741 $wrapper.removeClass(config.overlayedClass); 1742 } 1743 1744 // Add open transition class after element is set to fixed 1745 // so CSS animation is applied correctly 1746 setTimeout(function() { 1747 $siteHeader.addClass(config.openTransitionClass); 1748 }, 100); 1749 } else { 1750 if (!config.stickyActive) { 1751 return; 1752 } 1753 1754 config.stickyActive = false; 1755 1756 $siteHeader.removeClass(config.openTransitionClass).removeClass(config.stickyClass); 1757 if (config.wrapperOverlayed) { 1758 $wrapper.addClass(config.overlayedClass); 1759 } 1760 } 1761 1762 config.lastScroll = scroll; 1763 } 1764 1765 // If the header setting to overlay the menu on the collection image 1766 // is enabled but the collection setting is disabled, we need to undo 1767 // the init of the sticky nav 1768 function disableOverlayHeader() { 1769 $(selectors.wrapper) 1770 .removeClass(config.overlayEnabledClass) 1771 .removeClass(config.overlayedClass); 1772 config.wrapperOverlayed = false; 1773 } 1774 1775 return { 1776 init: init, 1777 disableOverlayHeader: disableOverlayHeader, 1778 unload: unload 1779 }; 1780 })(); 1781 1782 theme.articleImages = (function() { 1783 1784 var cache = {}; 1785 1786 function init() { 1787 cache.$rteImages = $('.rte--indented-images'); 1788 1789 if (!cache.$rteImages.length) { 1790 return; 1791 } 1792 1793 $(window).on('load', setImages); 1794 } 1795 1796 function setImages() { 1797 cache.$rteImages.find('img').each(function() { 1798 var $el = $(this); 1799 var attr = $el.attr('style'); 1800 1801 // Check if undefined or float: none 1802 if (!attr || attr == 'float: none;') { 1803 // Remove grid-breaking styles if image isn't wider than parent 1804 if ($el.width() < cache.$rteImages.width()) { 1805 $el.addClass('rte__no-indent'); 1806 } 1807 } 1808 }); 1809 } 1810 1811 return { 1812 init: init 1813 }; 1814 })(); 1815 1816 theme.Slideshow = (function() { 1817 this.$slideshow = null; 1818 1819 var classes = { 1820 next: 'is-next', 1821 init: 'is-init', 1822 wrapper: 'slideshow-wrapper', 1823 slideshow: 'slideshow', 1824 currentSlide: 'slick-current', 1825 pauseButton: 'slideshow__pause', 1826 isPaused: 'is-paused' 1827 }; 1828 1829 function slideshow(el, args) { 1830 this.$slideshow = $(el); 1831 this.$wrapper = this.$slideshow.closest('.' + classes.wrapper); 1832 this.$pause = this.$wrapper.find('.' + classes.pauseButton); 1833 1834 this.settings = { 1835 accessibility: true, 1836 arrows: args.arrows ? true : false, 1837 dots: args.dots ? true : false, 1838 draggable: true, 1839 touchThreshold: 5, 1840 pauseOnHover: false, 1841 autoplay: this.$slideshow.data('autoplay'), 1842 autoplaySpeed: this.$slideshow.data('speed') 1843 }; 1844 1845 this.$slideshow.on('init', this.init.bind(this)); 1846 1847 this.$slideshow.slick(this.settings); 1848 1849 this.$pause.on('click', this._togglePause.bind(this)); 1850 } 1851 1852 slideshow.prototype = $.extend({}, slideshow.prototype, { 1853 init: function(event, obj) { 1854 this.$slideshowList = obj.$list; 1855 this.$slickDots = obj.$dots; 1856 this.$allSlides = obj.$slides; 1857 this.slideCount = obj.slideCount; 1858
1859 this.$slideshow.addClass(classes.init); 1860 this._a11y(); 1861 this._clonedLazyloading(); 1862 }, 1863 destroy: function() { 1864 this.$slideshow.slick('unslick'); 1865 }, 1866 1867 // Playback 1868 _play: function() { 1869 this.$slideshow.slick('slickPause'); 1870 $(classes.pauseButton).addClass('is-paused'); 1871 }, 1872 _pause: function() { 1873 this.$slideshow.slick('slickPlay'); 1874 $(classes.pauseButton).removeClass('is-paused'); 1875 }, 1876 _togglePause: function() { 1877 var slideshowSelector = this._getSlideshowId(this.$pause); 1878 if (this.$pause.hasClass(classes.isPaused)) { 1879 this.$pause.removeClass(classes.isPaused); 1880 $(slideshowSelector).slick('slickPlay'); 1881 } else { 1882 this.$pause.addClass(classes.isPaused); 1883 $(slideshowSelector).slick('slickPause'); 1884 } 1885 }, 1886 1887 // Helpers 1888 _getSlideshowId: function($el) { 1889 return '#Slideshow-' + $el.data('id'); 1890 }, 1891 _activeSlide: function() { 1892 return this.$slideshow.find('.slick-active'); 1893 }, 1894 _currentSlide: function() { 1895 return this.$slideshow.find('.slick-current'); 1896 }, 1897 _nextSlide: function(index) { 1898 return this.$slideshow.find('.slideshow__slide[data-slick-index="' + index + '"]'); 1899 }, 1900 1901 // a11y fixes 1902 _a11y: function() { 1903 var $list = this.$slideshowList; 1904 var autoplay = this.settings.autoplay; 1905 1906 if (!$list) { 1907 return; 1908 } 1909 1910 // Remove default Slick aria-live attr until slider is focused 1911 $list.removeAttr('aria-live'); 1912 1913 // When an element in the slider is focused 1914 // pause slideshow and set aria-live 1915 $(classes.wrapper).on('focusin', function(evt) { 1916 if (!$(classes.wrapper).has(evt.target).length) { 1917 return; 1918 } 1919 1920 $list.attr('aria-live', 'polite'); 1921 if (autoplay) { 1922 this._pause(); 1923 } 1924 }.bind(this)); 1925 1926 // Resume autoplay 1927 $(classes.wrapper).on('focusout', function(evt) { 1928 if (!$(classes.wrapper).has(evt.target).length) { 1929 return; 1930 } 1931 1932 $list.removeAttr('aria-live'); 1933 if (autoplay) { 1934 this._play(); 1935 } 1936 }.bind(this)); 1937 }, 1938 1939 // Make sure lazyloading works on cloned slides 1940 _clonedLazyloading: function() { 1941 var $slideshow = this.$slideshow; 1942 1943 $slideshow.find('.slick-slide').each(function(index, el) { 1944 var $slide = $(el); 1945 if ($slide.hasClass('slick-cloned')) { 1946 var slideId = $slide.data('id'); 1947 var $slideImg = $slide.find('.hero__image').removeClass('lazyloading').addClass('lazyloaded'); 1948 1949 // Get inline style attribute from non-cloned slide with arbitrary timeout 1950 // so the image is loaded 1951 setTimeout(function() { 1952 var loadedImageStyle = $slideshow.find('.slideshow__slide--' + slideId + ':not(.slick-cloned) .hero__image').attr('style'); 1953 1954 if (loadedImageStyle) { 1955 $slideImg.attr('style', loadedImageStyle); 1956 } 1957 1958 }, this.settings.autoplaySpeed / 1.5); 1959 1960 } 1961 }.bind(this)); 1962 } 1963 }); 1964 1965 return slideshow; 1966 })(); 1967 1968 theme.announcementBar = (function() { 1969 var selectors = { 1970 bar: '.announcement', 1971 text: '.announcement__text', 1972 closeBtn: '.announcement__close' 1973 }; 1974 1975 var classes = { 1976 opening: 'announcement--opening', 1977 closed: 'announcement--closed' 1978 }; 1979 1980 var config = { 1981 namespace: '.announcementBar' 1982 }; 1983 1984 function init() { 1985 if (!$(selectors.closeBtn).length) { 1986 return; 1987 } 1988 1989 if (theme.config.hasSessionStorage && sessionStorage[announcementBarText()] !== 'hidden') { 1990 window.setTimeout(function() { 1991 announcementBarShow(); 1992 }, 2000); 1993 } 1994 1995 // Probably in the editor, so show it anyway 1996 if (!theme.config.hasSessionStorage) { 1997 window.setTimeout(function() { 1998 announcementBarShow(); 1999 }, 2000); 2000 } 2001 2002 $(selectors.closeBtn).on('click', function(evt) { 2003 evt.preventDefault(); 2004 announcementBarClose(); 2005 }); 2006 2007 $(window).on('resize' + config.namespace, $.debounce(150, announcementBarHeight)); 2008 } 2009 2010 function unload() { 2011 $(window).off(config.namespace); 2012 } 2013 2014 function announcementBarShow() {
2015 $(selectors.bar).removeClass(classes.closed); 2016 announcementBarHeight(); 2017 } 2018 2019 function announcementBarHeight() { 2020 var barHeight = $(selectors.text).outerHeight(true); 2021 $(selectors.bar).addClass(classes.opening).css({'max-height': barHeight}); 2022 } 2023 2024 function announcementBarClose() { 2025 if (theme.config.hasSessionStorage) { 2026 sessionStorage.setItem(announcementBarText(), 'hidden'); 2027 } 2028 2029 $(selectors.bar).addClass(classes.closed); 2030 } 2031 2032 function announcementBarText() { 2033 return $(selectors.text).data('text'); 2034 } 2035 2036 return { 2037 init: init, 2038 unload: unload 2039 }; 2040 })(); 2041 2042 theme.currencySwitcher = (function() { 2043 2044 var selectors = { 2045 dataDiv: '#CurrencyData', 2046 picker: '.currency-input' 2047 }; 2048 2049 var data = {}; 2050 2051 function init() { 2052 var $dataDiv = $(selectors.dataDiv); 2053 2054 if (!$dataDiv.length) { 2055 return; 2056 } 2057 2058 $primaryPicker = $('#CurrencyPicker-header'); 2059 $drawerPicker = $('#CurrencyPicker-drawer'); 2060 2061 // Keep all currency pickers in sync 2062 $primaryPicker.on('change', function() { 2063 $drawerPicker.val($(this).val()); 2064 }); 2065 2066 $drawerPicker.on('change', function() { 2067 $primaryPicker.val($(this).val()); 2068 }); 2069 2070 data = { 2071 currency: $dataDiv.data('shop-currency'), 2072 default: $dataDiv.data('default-currency'), 2073 format: $dataDiv.data('format'), 2074 moneyFormat: $dataDiv.data('money-format'), 2075 moneyCurrencyFormat: $dataDiv.data('money-currency-format') 2076 }; 2077 2078 Currency.format = data.format; 2079 2080 // Rely on the shop's currency format, not Shopify defaults (in case merchant changes it) 2081 Currency.money_format[data.currency] = data.moneyFormat; 2082 Currency.money_with_currency_format[data.currency] = data.moneyCurrencyFormat; 2083 2084 // Fix for customer account page 2085 $('span.money span.money').each(function() { 2086 $(this).parents('span.money').removeClass('money'); 2087 }); 2088 2089 // Save current price 2090 $('span.money').each(function() { 2091 $(this).attr('data-currency-' + data.currency, $(this).html()); 2092 }); 2093 2094 checkCookie(); 2095 2096 $(selectors.picker).val(Currency.currentCurrency).on('change', refresh); 2097 } 2098 2099 function refresh() { 2100 var newCurrency = $(selectors.picker).val(); 2101 Currency.convertAll(Currency.currentCurrency, newCurrency); 2102 } 2103 2104 function ajaxrefresh() { 2105 var newCurrency = $(selectors.picker).val(); 2106 // Ajax cart always returns shop's currency, not what theme settings defines 2107 Currency.convertAll(data.currency, newCurrency); 2108 } 2109 2110 function checkCookie() { 2111 var cookieCurrency = Currency.cookie.read(); 2112 2113 if (cookieCurrency == null) { 2114 if (data.currency !== data.default) { 2115 Currency.convertAll(data.currency, data.default); 2116 } 2117 else { 2118 Currency.currentCurrency = data.default; 2119 } 2120 } else if ($(selectors.picker).length && $(selectors.picker).find('option[value=' + cookieCurrency + ']').length === 0) { 2121 // If the cookie value does not correspond to any value in the currency dropdown 2122 Currency.currentCurrency = data.currency; 2123 Currency.cookie.write(data.currency); 2124 } 2125 else if (cookieCurrency === data.currency) { 2126 Currency.currentCurrency = data.currency; 2127 } 2128 else { 2129 Currency.convertAll(data.currency, cookieCurrency); 2130 } 2131 } 2132 2133 return { 2134 init: init, 2135 refresh: refresh, 2136 ajaxrefresh: ajaxrefresh 2137 }; 2138 })(); 2139 2140 theme.initQuickShop = function(reinit) { 2141 var ids = []; 2142 var $buttons = $('.quick-product__btn'); 2143 2144 $buttons.each(function() { 2145 var id = $(this).data('product-id'); 2146 var modalId = 'QuickShopModal-' + id; 2147 var name = 'quick-modal-' + id; 2148 2149 // If another identical modal exists, remove duplicates 2150 if (ids.indexOf(id) > -1) { 2151 $('.modal--quick-shop[data-product-id="'+ id +'"]').each(function(i) { 2152 if (i > 0) { 2153 $(this).remove(); 2154 } 2155 }); 2156 return; 2157 } 2158 2159 new theme.Modals(modalId, name); 2160 ids.push(id); 2161 }); 2162 }; 2163 2164 theme.videoModal = function() { 2165 var videoModalPlayer = null; 2166 var videoOptions = { 2167 width: 1280, 2168 height: 720, 2169 playerVars: { 2170 autohide: 0, 2171 autoplay: 1, 2172 branding: 0, 2173 cc_load_policy: 0, 2174 fs: 0, 2175 iv_load_policy: 3, 2176 modestbranding: 1, 2177 playsinline: 1, 2178 quality: 'hd720', 2179 rel: 0, 2180 showinfo: 0, 2181 wmode: 'opaque' 2182 } 2183 }; 2184 2185 var selectors = { 2186 triggers: 'a[href*="youtube.com/watch"], a[href*="youtu.be/"]' 2187 }; 2188 2189 if (!$(selectors.triggers).length) { 2190 return; 2191 } 2192 2193 var modal = new theme.Modals('VideoModal', 'video-modal', { 2194 closeOffContentClick: true, 2195 solid: true 2196 }); 2197 2198 $(selectors.triggers).on('click', triggerYouTubeModal); 2199 2200 function triggerYouTubeModal(evt) { 2201 evt.preventDefault(); 2202 window.loadYouTube(); 2203 2204 if (theme.config.youTubeReady) { 2205 startVideoOnClick(evt); 2206 } else { 2207 $('body').on('youTubeReady', function() { 2208 startVideoOnClick(evt); 2209 }); 2210 } 2211 } 2212 2213 function startVideoOnClick(evt) { 2214 var $el = $(evt.currentTarget); 2215 var videoId = getYoutubeVideoId($el.attr('href')); 2216 2217 var args = $.extend({}, videoOptions, { 2218 videoId: videoId 2219 }); 2220 2221 // Disable plays inline on mobile 2222 args.playerVars.playsinline = theme.config.bpSmall ? 0 : 1; 2223 2224 var videoModalPlayer = new YT.Player('VideoHolder', args); 2225 modal.open(); 2226 2227 $('body').on('modalClose.VideoModal', function() { 2228 // Slight timeout so it is destroyed after the modal closes 2229 setTimeout(function() { 2230 videoModalPlayer.destroy(); 2231 }, 500); // modal close css transition 2232 }); 2233 } 2234 2235 function getYoutubeVideoId(url) { 2236 var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/; 2237 var match = url.match(regExp); 2238 return (match&&match[7].length==11)? match[7] : false;
2239 } 2240 }; 2241 2242 2243 2244 theme.customerTemplates = (function() { 2245 2246 function initEventListeners() { 2247 // Show reset password form 2248 $('#RecoverPassword').on('click', function(evt) { 2249 evt.preventDefault(); 2250 toggleRecoverPasswordForm(); 2251 }); 2252 2253 // Hide reset password form 2254 $('#HideRecoverPasswordLink').on('click', function(evt) { 2255 evt.preventDefault(); 2256 toggleRecoverPasswordForm(); 2257 }); 2258 } 2259 2260 /** 2261 * 2262 * Show/Hide recover password form 2263 * 2264 */ 2265 function toggleRecoverPasswordForm() { 2266 $('#RecoverPasswordForm').toggleClass('hide'); 2267 $('#CustomerLoginForm').toggleClass('hide'); 2268 } 2269 2270 /** 2271 * 2272 * Show reset password success message 2273 * 2274 */ 2275 function resetPasswordSuccess() { 2276 var $formState = $('.reset-password-success'); 2277 2278 // check if reset password form was successfully submitted 2279 if (!$formState.length) { 2280 return; 2281 } 2282 2283 // show success message 2284 $('#ResetSuccess').removeClass('hide'); 2285 } 2286 2287 /** 2288 * 2289 * Show/hide customer address forms 2290 * 2291 */ 2292 function customerAddressForm() { 2293 var $newAddressForm = $('#AddressNewForm'); 2294 var $addressForms = $('.js-address-form'); 2295 2296 if (!$newAddressForm.length || !$addressForms.length) { 2297 return; 2298 } 2299 2300 if (Shopify) { 2301 $('.js-address-country').each(function() { 2302 var $container = $(this); 2303 var countryId = $container.data('country-id'); 2304 var provinceId = $container.data('province-id'); 2305 var provinceContainerId = $container.data('province-container-id'); 2306 2307 new Shopify.CountryProvinceSelector( 2308 countryId, 2309 provinceId, 2310 { 2311 hideElement: provinceContainerId 2312 } 2313 ); 2314 }); 2315 } 2316 2317 // Toggle new/edit address forms 2318 $('.address-new-toggle').on('click', function() { 2319 $newAddressForm.toggleClass('hide'); 2320 }); 2321 2322 $('.address-edit-toggle').on('click', function() { 2323 var formId = $(this).data('form-id'); 2324 $('#EditAddress_' + formId).toggleClass('hide'); 2325 }); 2326 2327 $('.address-delete').on('click', function() { 2328 var $el = $(this); 2329 var formId = $el.data('form-id'); 2330 var confirmMessage = $el.data('confirm-message'); 2331 2332 if (confirm(confirmMessage || 'Are you sure you wish to delete this address?')) { 2333 Shopify.postLink('/account/addresses/' + formId, {parameters: {_method: 'delete'}}); 2334 } 2335 }); 2336 } 2337 2338 /** 2339 * 2340 * Check URL for reset password hash 2341 * 2342 */ 2343 function checkUrlHash() { 2344 var hash = window.location.hash; 2345 2346 // Allow deep linking to recover password form 2347 if (hash === '#recover') { 2348 toggleRecoverPasswordForm(); 2349 } 2350 } 2351 2352 return { 2353 init: function() { 2354 checkUrlHash(); 2355 initEventListeners(); 2356 resetPasswordSuccess(); 2357 customerAddressForm(); 2358 } 2359 }; 2360 })(); 2361 2362 2363 theme.Product = (function() { 2364 2365 var classes = { 2366 onSale: 'on-sale', 2367 disabled: 'disabled', 2368 isModal: 'is-modal', 2369 loading: 'loading', 2370 loaded: 'loaded', 2371 interactable: 'video-interactable' 2372 }; 2373 2374 var selectors = { 2375 productVideo: '.product__video', 2376 videoParent: '.product__video-wrapper', 2377 currentSlide: '.slick-current' 2378 }; 2379 2380 var youtubeReady; 2381 var videos = {}; 2382 var youtubePlayers = []; 2383 var youtubeVideoOptions = { 2384 height: '480', 2385 width: '850', 2386 playerVars :{ 2387 autohide: 0, 2388 autoplay: 1, 2389 branding: 0, 2390 cc_load_policy: 0, 2391 controls: 0, 2392 fs: 0, 2393 iv_load_policy: 3, 2394 modestbranding: 1, 2395 playsinline: 1, 2396 quality: 'hd720', 2397 rel: 0, 2398 showinfo: 0, 2399 wmode: 'opaque' 2400 }, 2401 events: { 2402 onReady: onVideoPlayerReady, 2403 onStateChange: onVideoStateChange 2404 } 2405 }; 2406 2407 var vimeoReady; 2408 var vimeoPlayers = []; 2409 var vimeoVideoOptions = { 2410 byline: false, 2411 title: false, 2412 portrait: false, 2413 loop: true 2414 }; 2415 2416 function onVideoPlayerReady(evt) { 2417 var $player = $(evt.target.a); 2418 var playerId = $player.attr('id'); 2419 youtubePlayers[playerId] = evt.target; // update stored player 2420 var player = youtubePlayers[playerId]; 2421 2422 setParentAsLoading($player); 2423 2424 if (videos[playerId].style === 'muted') { 2425 youtubePlayers[playerId].mute().playVideo().pauseVideo(); 2426 } else { 2427 setParentAsLoaded($player); 2428 } 2429 2430 // If first slide or only photo, start video 2431 if ($player.closest(selectors.currentSlide).length || $player.data('image-count') === 1) { 2432 if (videos[playerId].style === 'muted') { 2433 youtubePlayers[playerId].playVideo(); 2434 initCheckVisibility(playerId); 2435 } 2436 } 2437 } 2438 2439 function initCheckVisibility(playerId) { 2440 // Add out of view pausing 2441 videoVisibilityCheck(playerId); 2442 $(window).on('scroll.' + playerId, {id: playerId}, $.throttle(150, videoVisibilityCheck)); 2443 } 2444 2445 function videoVisibilityCheck(id) { 2446 var playerId; 2447 2448 if (typeof id === 'string') { 2449 playerId = id; 2450 } else { 2451 // Data comes in as part of the scroll event 2452 playerId = id.data.id; 2453 } 2454 2455 if (theme.isElementVisible($('#' + playerId))) { 2456 if (videos[playerId] && videos[playerId].style === 'unmuted') { 2457 return; 2458 } 2459 playVisibleVideo(playerId); 2460 } else { 2461 pauseHiddenVideo(playerId); 2462 } 2463 } 2464 2465 function playVisibleVideo(id) { 2466 if (youtubePlayers[id] && typeof youtubePlayers[id].playVideo === 'function') { 2467 youtubePlayers[id].playVideo(); 2468 } 2469 } 2470 2471 function pauseHiddenVideo(id) { 2472 if (youtubePlayers[id] && typeof youtubePlayers[id].pauseVideo === 'function') { 2473 youtubePlayers[id].pauseVideo(); 2474 } 2475 } 2476 2477 function onVideoStateChange(evt) { 2478 var $player = $(evt.target.a); 2479 var playerId = $player.attr('id'); 2480 var player = youtubePlayers[playerId]; 2481 2482 switch (evt.data) { 2483 case -1: // unstarted 2484 // Handle low power state on iOS by checking if 2485 // video is reset to unplayed after attempting to buffer 2486 if (videos[playerId].attemptedToPlay) { 2487 setParentAsLoaded($player); 2488 setVideoToBeInteractedWith($player); 2489 } 2490 break; 2491 case 0: // ended 2492 player.playVideo(); 2493 break; 2494 case 1: // playing 2495 setParentAsLoaded($player); 2496 break; 2497 case 3: // buffering 2498 videos[playerId].attemptedToPlay = true; 2499 break; 2500 } 2501 } 2502 2503 function setParentAsLoading($el) { 2504 $el 2505 .closest(selectors.videoParent)
2506 .addClass(classes.loading); 2507 } 2508 2509 function setParentAsLoaded($el) { 2510 $el 2511 .closest(selectors.videoParent) 2512 .removeClass(classes.loading) 2513 .addClass(classes.loaded); 2514 } 2515 2516 function setVideoToBeInteractedWith($el) { 2517 $el 2518 .closest(selectors.videoParent) 2519 .addClass(classes.interactable); 2520 } 2521 2522 function Product(container) { 2523 var $container = this.$container = $(container); 2524 var sectionId = this.sectionId = $container.attr('data-section-id'); 2525 2526 this.inModal = $container.closest('.modal').length; 2527 this.$modal; 2528 2529 this.settings = { 2530 enableHistoryState: $container.data('enable-history-state') || false, 2531 namespace: '.product-' + sectionId, 2532 inventory: $container.data('inventory') || false, 2533 modalInit: false, 2534 slickMainInitialized: false, 2535 slickThumbInitialized: false, 2536 hasImages: true, 2537 hasMultipleImages: false, 2538 imageSize: '620x' 2539 }; 2540 2541 // Overwrite some settings when loaded in modal 2542 if (this.inModal) { 2543 this.settings.enableHistoryState = false; 2544 this.settings.namespace = '.product-' + sectionId + '-modal'; 2545 this.$modal = $('#QuickShopModal-' + sectionId); 2546 } 2547 2548 this.selectors = { 2549 variantsJson: 'VariantsJson-' + sectionId, 2550 currentVariantJson: 'CurrentVariantJson-' + sectionId, 2551 2552 video: 'ProductVideo-' + sectionId, 2553 photoThumbs: '.product__thumb-' + sectionId, 2554 thumbSlider: '#ProductThumbs-' + sectionId, 2555 mainSlider: '#ProductPhotos-' + sectionId, 2556 imageContainer: '[data-product-images]', 2557 productImageMain: '.product-image-main--' + sectionId, 2558 2559 priceWrapper: '.product__price-wrap-' + sectionId, 2560 price: '#ProductPrice-' + sectionId, 2561 comparePrice: '#ComparePrice-' + sectionId, 2562 priceA11y: '#PriceA11y-' + sectionId, 2563 comparePriceA11y: '#ComparePriceA11y-' + sectionId, 2564 sku: '#Sku-' + sectionId, 2565 inventory: '#ProductInventory-' + sectionId, 2566 2567 addToCart: '#AddToCart-' + sectionId, 2568 addToCartText: '#AddToCartText-' + sectionId, 2569 2570 originalSelectorId: '#ProductSelect-' + sectionId, 2571 singleOptionSelector: '.variant__input-' + sectionId, 2572 variantColorSwatch: '.variant__input--color-swatch-' + sectionId, 2573 2574 modalFormHolder: '#ProductFormHolder-' + sectionId, 2575 formContainer: '#AddToCartForm-' + sectionId 2576 }; 2577 2578 this.$mainSlider = $(this.selectors.mainSlider); 2579 this.$thumbSlider = $(this.selectors.thumbSlider); 2580 this.$firstProductImage = this.$mainSlider.find('img').first(); 2581 2582 if (!this.$firstProductImage.length) { 2583 this.settings.hasImages = false; 2584 } 2585 2586 this.init(); 2587 } 2588 2589 Product.prototype = $.extend({}, Product.prototype, { 2590 init: function() { 2591 if (this.inModal) {
2592 this.$container.addClass(classes.isModal); 2593 $('body') 2594 .off('modalOpen.QuickShopModal-' + this.sectionId) 2595 .off('modalClose.QuickShopModal-' + this.sectionId); 2596 $('body').on('modalOpen.QuickShopModal-' + this.sectionId, this.openModalProduct.bind(this)); 2597 $('body').on('modalClose.QuickShopModal-' + this.sectionId, this.closeModalProduct.bind(this)); 2598 } 2599 2600 if (!this.inModal) { 2601 this.stringOverrides(); 2602 this.formSetup(); 2603 this.productSetup(); 2604 2605 this.checkIfVideos(); 2606 this.createImageCarousels(); 2607 } 2608 }, 2609 2610 formSetup: function() { 2611 // Determine how to handle variant availability selectors 2612 if (theme.settings.dynamicVariantsEnable) { 2613 if (theme.settings.dynamicVariantType === 'dropdown') { 2614 this.$variantSelectors = { 2615 all: $(this.selectors.formContainer).find('select'), 2616 selected: $(this.selectors.formContainer).find('select') 2617 } 2618 } else { 2619 this.$variantSelectors = { 2620 all: $(this.selectors.formContainer).find('input'), 2621 selected: $(this.selectors.formContainer).find('input:checked') 2622 } 2623 } 2624 } 2625 2626 this.initQtySelector(); 2627 this.initAjaxProductForm(); 2628 this.initVariants(); 2629 }, 2630 2631 productSetup: function() { 2632 this.setImageSizes(); 2633 this.initImageSwitch(); 2634 this.initImageZoom(); 2635 }, 2636 2637 stringOverrides: function() { 2638 theme.productStrings = theme.productStrings || {}; 2639 $.extend(theme.strings, theme.productStrings); 2640 }, 2641 2642 setImageSizes: function() { 2643 if (!this.settings.hasImages) { 2644 return; 2645 } 2646 2647 // Get srcset image src, works on most modern browsers 2648 // otherwise defaults to settings.imageSize 2649 var currentImage = this.$firstProductImage[0].currentSrc; 2650 2651 if (currentImage) { 2652 this.settings.imageSize = theme.Images.imageSize(currentImage); 2653 } 2654 }, 2655 2656 initVariants: function() { 2657 if (!document.getElementById(this.selectors.variantsJson)) { 2658 return; 2659 } 2660 2661 this.variantsObject = JSON.parse(document.getElementById(this.selectors.variantsJson).innerHTML); 2662 2663 var options = { 2664 $container: this.$container, 2665 enableHistoryState: this.settings.enableHistoryState, 2666 singleOptionSelector: this.selectors.singleOptionSelector, 2667 originalSelectorId: this.selectors.originalSelectorId, 2668 variants: this.variantsObject 2669 }; 2670 2671 if ($(this.selectors.variantColorSwatch).length) { 2672 $(this.selectors.variantColorSwatch).on('change', function(evt) { 2673 var $el = $(evt.currentTarget); 2674 var color = $el.data('color-name'); 2675 var index = $el.data('color-index'); 2676 this.updateColorName(color, index); 2677 }.bind(this)); 2678 } 2679 2680 this.variants = new slate.Variants(options); 2681 this.$container.on('variantChange' + this.settings.namespace, this.updateCartButton.bind(this)); 2682 this.$container.on('variantImageChange' + this.settings.namespace, this.updateVariantImage.bind(this)); 2683 this.$container.on('variantPriceChange' + this.settings.namespace, this.updatePrice.bind(this)); 2684 if ($(this.selectors.sku).length) { 2685 this.$container.on('variantSKUChange' + this.settings.namespace, this.updateSku.bind(this)); 2686 } 2687 if (this.settings.inventory) { 2688 this.$container.on('variantChange' + this.settings.namespace, this.updateInventory.bind(this)); 2689 } 2690 2691 // Update individual variant availability on each selection 2692 if (theme.settings.dynamicVariantsEnable && document.getElementById(this.selectors.currentVariantJson)) { 2693 this.currentVariantObject = JSON.parse(document.getElementById(this.selectors.currentVariantJson).innerHTML); 2694 2695 this.$variantSelectors.all.on('change' + this.settings.namespace, this.updateV
2695ariantAvailability.bind(this)); 2696 2697 // Set default state based on current selected variant 2698 this.setCurrentVariantAvailability(this.currentVariantObject, true); 2699 } 2700 }, 2701 2702 // Variant change functions 2703 updateColorName: function(color, index) { 2704 // Updates on radio button change, not variant.js 2705 $('#VariantColorLabel-' + this.sectionId + '-' + index).text(color); 2706 }, 2707 2708 setCurrentVariantAvailability: function(variant) { 2709 var valuesToEnable = { 2710 option1: [], 2711 option2: [], 2712 option3: [] 2713 }; 2714 2715 // Disable all options to start 2716 this.disableVariantGroup($(this.selectors.formContainer).find('.variant-input-wrap')); 2717 2718 // Combine all available variants 2719 var availableVariants = this.variantsObject.filter(function(el) { 2720 if (variant.id === el.id) { 2721 return false; 2722 } 2723 2724 // Option 1 2725 if (variant.option2 === el.option2 && variant.option3 === el.option3) { 2726 return true; 2727 } 2728 2729 // Option 2 2730 if (variant.option1 === el.option1 && variant.option3 === el.option3) { 2731 return true; 2732 } 2733 2734 // Option 3 2735 if (variant.option1 === el.option1 && variant.option2 === el.option2) { 2736 return true; 2737 } 2738 }); 2739 2740 2741 // IE11 can't handle shortform of {variant} so extra step is needed 2742 var variantObject = { 2743 variant: variant 2744 }; 2745 2746 availableVariants = Object.assign({}, variantObject, availableVariants); 2747 2748 // Loop through each available variant to gather variant values 2749 for (var property in availableVariants) { 2750 if (availableVariants.hasOwnProperty(property)) { 2751 var item = availableVariants[property]; 2752 var option1 = item.option1; 2753 var option2 = item.option2; 2754 var option3 = item.option3; 2755 2756 if (option1) { 2757 if (valuesToEnable.option1.indexOf(option1) === -1) { 2758 valuesToEnable.option1.push(option1); 2759 } 2760 } 2761 if (option2) { 2762 if (valuesToEnable.option2.indexOf(option2) === -1) { 2763 valuesToEnable.option2.push(option2); 2764 } 2765 } 2766 if (option3) { 2767 if (valuesToEnable.option3.indexOf(option3) === -1) { 2768 valuesToEnable.option3.push(option3); 2769 } 2770 } 2771 } 2772 } 2773 2774 // Have values to enable, separated by option index 2775 if (valuesToEnable.option1.length) { 2776 this.enableVariantOptionByValue(valuesToEnable.option1, 'option1'); 2777 } 2778 if (valuesToEnable.option2.length) { 2779 this.enableVariantOptionByValue(valuesToEnable.option2, 'option2'); 2780 } 2781 if (valuesToEnable.option3.length) { 2782 this.enableVariantOptionByValue(valuesToEnable.option3, 'option3'); 2783 } 2784 }, 2785 2786 updateVariantAvailability: function(evt, value, index) { 2787 if (value && index) { 2788 var newVal = value; 2789 var optionIndex = index; 2790 } else { 2791 var $el = $(evt.currentTarget); 2792 var newVal = $el.val() ? $el.val() : evt.currentTarget.value; 2793 var optionIndex = $el.data('index'); 2794 } 2795 2796 var variants = this.variantsObject.filter(function(el) { 2797 return el[optionIndex] === newVal; 2798 }); 2799 2800 // Disable all buttons/dropdown options that aren't the current index 2801 $(this.selectors.formContainer).find('.variant-input-wrap').each(function(index, el) { 2802 var $group = $(el); 2803 var currentOptionIndex = $group.data('index'); 2804 2805 if (currentOptionIndex !== optionIndex) { 2806 // Disable all options as a starting point 2807 this.disableVariantGroup($group); 2808 2809 // Loop through legit available options and enable 2810 for (var i = 0; i < variants.length; i++) { 2811 this.enableVariantOption($group, variants[i][currentOptionIndex]); 2812 } 2813 } 2814 }.bind(this)); 2815 }, 2816 2817 disableVariantGroup: function($group) { 2818 if (theme.settings.dynamicVariantType === 'dropdown') { 2819 $group.find('option').prop('disabled', true) 2820 } else { 2821 $group.find('input').prop('disabled', true); 2822 $group.find('label').toggleClass('disabled', true); 2823 } 2824 }, 2825 2826 enableVariantOptionByValue: function(array, index) { 2827 var $group = $(this.selectors.formContainer).find('.variant-input-wrap[data-index="'+ index +'"]'); 2828 2829 for (var i = 0; i < array.length; i++) { 2830 this.enableVariantOption($group, array[i]); 2831 } 2832 }, 2833 2834 enableVariantOption: function($group, value) { 2835 // Selecting by value so escape it 2836 value = value.replace(/([ #;&,.+*~\':"!^$[\]()=>|\/@])/g,'\\$1'); 2837 2838 if (theme.settings.dynamicVariantType === 'dropdown') { 2839 $group.find('option[value="'+ value +'"]').prop('disabled', false); 2840 } else { 2841 var $buttonGroup = $group.find('.variant-input[data-value="'+ value +'"]'); 2842 $buttonGroup.find('input').prop('disabled', false); 2843 $buttonGroup.find('label').toggleClass('disabled', false); 2844 } 2845 }, 2846 2847 updateCartButton: function(evt) { 2848 var variant = evt.variant; 2849 2850 if (variant) { 2851 if (variant.available) { 2852 // Available, enable the submit button and change text
2853 $(this.selectors.addToCart).removeClass(classes.disabled).prop('disabled', false); 2854 $(this.selectors.addToCartText).html(theme.strings.addToCart); 2855 } else { 2856 // Sold out, disable the submit button and change text 2857 $(this.selectors.addToCart).addClass(classes.disabled).prop('disabled', true); 2858 $(this.selectors.addToCartText).html(theme.strings.soldOut); 2859 } 2860 } else { 2861 // The variant doesn't exist, disable submit button 2862 $(this.selectors.addToCart).addClass(classes.disabled).prop('disabled', true); 2863 $(this.selectors.addToCartText).html(theme.strings.unavailable); 2864 } 2865 }, 2866 2867 updatePrice: function(evt) { 2868 var variant = evt.variant; 2869 2870 if (variant) { 2871 // Regular price 2872 $(this.selectors.price).html(theme.Currency.formatMoney(variant.price, theme.settings.moneyFormat)).show(); 2873 2874 // Sale price, if necessary 2875 if (variant.compare_at_price > variant.price) { 2876 $(this.selectors.comparePrice).html(theme.Currency.formatMoney(variant.compare_at_price, theme.settings.moneyFormat)); 2877 $(this.selectors.priceWrapper).removeClass('hide');
2878 $(this.selectors.price).addClass(classes.onSale); 2879 $(this.selectors.comparePriceA11y).attr('aria-hidden', 'false'); 2880 $(this.selectors.priceA11y).attr('aria-hidden', 'false'); 2881 } else { 2882 $(this.selectors.priceWrapper).addClass('hide'); 2883 $(this.selectors.price).removeClass(classes.onSale); 2884 $(this.selectors.comparePriceA11y).attr('aria-hidden', 'true'); 2885 $(this.selectors.priceA11y).attr('aria-hidden', 'true'); 2886 } 2887 2888 if (theme.settings.currenciesEnabled) { 2889 theme.currencySwitcher.ajaxrefresh(); 2890 } 2891 } 2892 }, 2893 2894 updateSku: function(evt) { 2895 var variant = evt.variant; 2896 var newSku = ''; 2897 2898 if (variant) { 2899 if (variant.sku) { 2900 newSku = variant.sku; 2901 } 2902 2903 $(this.selectors.sku).html(newSku); 2904 } 2905 }, 2906 2907 updateInventory: function(evt) { 2908 var variant = evt.variant; 2909 var showInventory = false; 2910 var quantity = 0; 2911 var $inventoryLabel = $(this.selectors.inventory); 2912 2913 if (variant && variant.inventory_management === 'shopify') { 2914 showInventory = true; 2915 quantity = window.inventories[this.sectionId][variant.id]; 2916 2917 if (quantity <= 0 || quantity > theme.settings.inventoryThreshold) { 2918 showInventory = false; 2919 } 2920 } 2921 2922 if (!showInventory) { 2923 $inventoryLabel.addClass('hide'); 2924 } else { 2925 $inventoryLabel 2926 .removeClass('hide') 2927 .text(theme.strings.stockLabel.replace('[count]', quantity)); 2928 } 2929 }, 2930 2931 updateVariantImage: function(evt) { 2932 var variant = evt.variant; 2933 var sizedImgUrl = theme.Images.getSizedImageUrl(variant.featured_image.src, this.settings.imageSize); 2934 2935 var $newImage = $('.product__thumb[data-id="' + variant.featured_image.id + '"]'); 2936 var imageIndex = this._slideIndex($newImage.closest('.product__thumb-item')); 2937 2938 // If there is no index, slider is not initalized 2939 if (typeof imageIndex === 'undefined') { 2940 return; 2941 } 2942 2943 this.$mainSlider.slick('slickGoTo', imageIndex); 2944 }, 2945 2946 // Image/thumbnail toggling 2947 initImageSwitch: function() { 2948 if (!$(this.selectors.photoThumbs).length) { 2949 return; 2950 } 2951 2952 var self = this; 2953 2954 $(this.selectors.photoThumbs).on('click', function(evt) { 2955 evt.preventDefault(); 2956 }); 2957 }, 2958 2959 initImageZoom: function() { 2960 var $container = $(this.selectors.imageContainer, this.$container); 2961 var imageZoom = new theme.Photoswipe($container[0], this.sectionId); 2962 }, 2963 2964 checkIfVideos: function() { 2965 var $productVideos = this.$mainSlider.find(selectors.productVideo); 2966 2967 // Stop if there are 0 videos 2968 if (!$productVideos.length) { 2969 return false; 2970 } 2971 2972 var videoTypes = []; 2973 2974 $productVideos.each(function() { 2975 var type = $(this).data('video-type'); 2976 2977 if (videoTypes.indexOf(type) < 0) { 2978 videoTypes.push(type); 2979 } 2980 }); 2981 2982 // Load YouTube API if not already loaded 2983 if (videoTypes.indexOf('youtube') > -1) { 2984 if (!theme.config.youTubeReady) { 2985 window.loadYouTube(); 2986 $('body').on('youTubeReady' + this.settings.namespace, function() { 2987 this.loadYoutubeVideos($productVideos); 2988 }.bind(this)); 2989 } else { 2990 this.loadYoutubeVideos($productVideos); 2991 } 2992 } 2993 2994 // Load Vimeo API if not already loaded 2995 if (videoTypes.indexOf('vimeo') > -1) { 2996 if (!vimeoReady) { 2997 window.loadVimeo(); 2998 $('body').on('vimeoReady' + this.settings.namespace, function() { 2999 this.loadVimeoVideos($productVideos); 3000 }.bind(this)) 3001 } else { 3002 this.loadVimeoVideos($productVideos); 3003 } 3004 } 3005 3006 // Add mp4 video players 3007 if (videoTypes.indexOf('mp4') > -1) { 3008 this.loadMp4Videos($productVideos); 3009 } 3010 3011 return videoTypes; 3012 }, 3013 3014 loadMp4Videos: function($videos) { 3015 $videos.each(function() { 3016 var $el = $(this); 3017 if ($el.data('video-type') != 'mp4') { 3018 return; 3019 } 3020 3021 var id = $el.attr('id'); 3022 var videoId = $el.data('video-id'); 3023 3024 videos[this.id] = { 3025 type: 'mp4', 3026 divId: id, 3027 style: $el.data('video-style') 3028 }; 3029 }); 3030 }, 3031 3032 loadVimeoVideos: function($videos) { 3033 $videos.each(function() { 3034 var $el = $(this); 3035 if ($el.data('video-type') != 'vimeo') { 3036 return; 3037 } 3038 3039 var id = $el.attr('id'); 3040 var videoId = $el.data('video-id'); 3041 3042 videos[this.id] = { 3043 type: 'vimeo', 3044 divId: id, 3045 id: videoId, 3046 style: $el.data('video-style'), 3047 width: $el.data('video-width'), 3048 height: $el.data('video-height') 3049 }; 3050 }); 3051 3052 // Create a new player for each Vimeo video 3053 for (var key in videos) { 3054 if (videos[key].type != 'vimeo') { 3055 continue; 3056 } 3057 3058 var args = $.extend({}, vimeoVideoOptions, videos[key]); 3059 vimeoPlayers[key] = new Vimeo.Player(videos[key].divId, args); 3060 } 3061 3062 vimeoReady = true; 3063 }, 3064 3065 autoplayVimeoVideo: function(id) {
3066 // Do not autoplay on mobile though 3067 if (!theme.config.bpSmall) { 3068 this.requestToPlayVimeoVideo(id); 3069 } else { 3070 // Set as loaded on mobile so you can see the image 3071 var $player = $('#' + id); 3072 setParentAsLoaded($player); 3073 } 3074 }, 3075 3076 requestToPlayVimeoVideo: function(id) { 3077 // The slider may initialize and attempt to play the video before 3078 // the API is even ready, because it sucks. 3079 3080 var $player = $('#' + id); 3081 setParentAsLoading($player); 3082 3083 if (!vimeoReady) { 3084 // Wait for the trigger, then play it 3085 $('body').on('vimeoReady' + this.settings.namespace, function() { 3086 this.playVimeoVideo(id); 3087 }.bind(this)) 3088 return; 3089 } 3090 3091 this.playVimeoVideo(id); 3092 }, 3093 3094 playVimeoVideo: function(id) { 3095 vimeoPlayers[id].play(); 3096 3097 if (videos[id].style === 'muted') { 3098 vimeoPlayers[id].setVolume(0); 3099 } 3100 3101 var $player = $('#' + id); 3102 setParentAsLoaded($player); 3103 }, 3104 3105 stopVimeoVideo: function(id) { 3106 if (!theme.config.vimeoReady) { 3107 return; 3108 } 3109 3110 if (id) { 3111 vimeoPlayers[id].pause(); 3112 } else { 3113 for (key in vimeoPlayers) { 3114 if (typeof vimeoPlayers[key].pause === 'function') { 3115 vimeoPlayers[key].pause(); 3116 } 3117 } 3118 } 3119 }, 3120 3121 loadYoutubeVideos: function($videos) { 3122 $videos.each(function() { 3123 var $el = $(this); 3124 if ($el.data('video-type') != 'youtube') { 3125 return; 3126 } 3127 3128 var id = $el.attr('id'); 3129 var videoId = $el.data('youtube-id'); 3130 3131 videos[this.id] = { 3132 type: 'youtube', 3133 id: id, 3134 videoId: videoId, 3135 style: $el.data('video-style'), 3136 width: $el.data('video-width'), 3137 height: $el.data('video-height'), 3138 attemptedToPlay: false 3139 }; 3140 }); 3141 3142 // Create a player for each YouTube video 3143 for (var key in videos) { 3144 if (videos[key].type === 'youtube') { 3145 if (videos.hasOwnProperty(key)) { 3146 var args = $.extend({}, youtubeVideoOptions, videos[key]); 3147 3148 if (args.style === 'muted') { 3149 // default youtubeVideoOptions, no need to change anything 3150 } else { 3151 args.playerVars.controls = 1; 3152 args.playerVars.autoplay = 0; 3153 } 3154 3155 youtubePlayers[key] = new YT.Player(key, args); 3156 } 3157 } 3158 } 3159 3160 youtubeReady = true; 3161 }, 3162 3163 requestToPlayYoutubeVideo: function(id, forcePlay) { 3164 if (!theme.config.youTubeReady) { 3165 return; 3166 } 3167 3168 var $player = $('#' + id); 3169 setParentAsLoading($player); 3170 3171 // If video is requested too soon, player might not be ready. 3172 // Set arbitrary timeout to request it again in a second 3173 if (typeof youtubePlayers[id].playVideo != 'function') { 3174 var o = this; 3175 setTimeout(function() { 3176 o.playYoutubeVideo(id, forcePlay); 3177 }, 1000); 3178 return; 3179 } 3180 3181 this.playYoutubeVideo(id, forcePlay); 3182 }, 3183 3184 playYoutubeVideo: function (id, forcePlay) { 3185 var $player = $('#' + id); 3186 setParentAsLoaded($player); 3187 if (typeof youtubePlayers[id].playVideo === 'function') { 3188 youtubePlayers[id].playVideo(); 3189 } 3190 3191 // forcePlay is sent as true from beforeSlideChange so the visibility 3192 // check isn't fooled by the next slide positioning 3193 if (!forcePlay) { 3194 initCheckVisibility(id); 3195 } 3196 }, 3197 3198 stopYoutubeVideo: function(id) { 3199 if (!theme.config.youTubeReady) { 3200 return; 3201 } 3202 3203 if (id && youtubePlayers[id]) { 3204 if (typeof youtubePlayers[id].pauseVideo === 'function') { 3205 youtubePlayers[id].pauseVideo(); 3206 } 3207 $(window).off('scroll.' + id); 3208 } else {
3209 for (key in youtubePlayers) { 3210 if (typeof youtubePlayers[key].pauseVideo === 'function') { 3211 youtubePlayers[key].pauseVideo(); 3212 $(window).off('scroll.' + key); 3213 } 3214 } 3215 } 3216 }, 3217 3218 playMp4Video: function(id) { 3219 var $player = $('#' + id); 3220 setParentAsLoaded($player); 3221 3222 $player[0].play(); 3223 }, 3224 3225 stopMp4Video: function(id) { 3226 if (id) { 3227 $('#' + id)[0].pause(); 3228 } else { 3229 // loop through all mp4 videos to stop them 3230 for (var key in videos) { 3231 if (videos[key].type === 'mp4') { 3232 var player = $('#' + videos[key].divId)[0]; 3233 if (typeof player.pause === 'function') { 3234 player.pause(); 3235 } 3236 } 3237 } 3238 } 3239 }, 3240 3241 createImageCarousels: function() { 3242 if (!this.$thumbSlider.length || $(this.selectors.photoThumbs).length < 2) { 3243 // Single product image. Init video if it exists 3244 var $video = $(this.selectors.productImageMain).find(selectors.productVideo); 3245 if ($video.length) { 3246 this.initVideo($video); 3247 } 3248 3249 return; 3250 } 3251 3252 this.settings.hasMultipleImages = true; 3253 3254 // Set starting slide (for both sliders) 3255 var $activeSlide = this.$mainSlider.find('.starting-slide'); 3256 var startIndex = this._slideIndex($activeSlide); 3257 3258 // Lame way to prevent duplicate event listeners 3259 this.$mainSlider.off('init'); 3260 this.$mainSlider.off('beforeChange'); 3261 this.$mainSlider.on('init', this.mainSlideInit.bind(this)); 3262 this.$mainSlider.on('beforeChange', this.beforeSlideChange.bind(this)); 3263 this.$thumbSlider.on('init', this.thumbSlideInit.bind(this)); 3264 3265 var thumbnailsVertical = this.$thumbSlider.data('position') === 'beside' ? true : false; 3266 3267 this.$mainSlider.slick({ 3268 adaptiveHeight: thumbnailsVertical ? false : true, 3269 asNavFor: this.selectors.thumbSlider, 3270 infinite: false, 3271 arrows: false, 3272 dots: false, 3273 initialSlide: startIndex 3274 }); 3275 3276 this.$thumbSlider.slick({ 3277 asNavFor: this.selectors.mainSlider, 3278 slidesToShow: thumbnailsVertical ? 3 : 5, 3279 slidesToScroll: 1, 3280 arrows: false, 3281 dots: false, 3282 vertical: thumbnailsVertical, 3283 verticalSwiping: thumbnailsVertical, 3284 focusOnSelect: true, 3285 infinite: false, 3286 customHeightMatching: thumbnailsVertical, 3287 customSlideAdvancement: true, 3288 initialSlide: startIndex 3289 }); 3290 3291 // Too many thumbnails can cause the AOS calculations to be off 3292 // so refresh that when the slider is ready 3293 if (AOS) { 3294 AOS.refresh(); 3295 } 3296 }, 3297 3298 destroyImageCarousels: function() { 3299 if (this.$mainSlider && this.settings.slickMainInitialized) { 3300 this.$mainSlider.slick('unslick'); 3301 this.settings.slickMainInitialized = false; 3302 } 3303 3304 if (this.$thumbSlider && this.settings.slickThumbInitialized) { 3305 this.$thumbSlider.slick('unslick'); 3306 this.settings.slickThumbInitialized = false; 3307 } 3308 3309 this.settings.slickMainInitialized = false; 3310 this.settings.slickThumbInitialized = false; 3311 }, 3312 3313 mainSlideInit: function(event, slick) { 3314 var $slider = slick.$slider; 3315 var $currentSlide = $slider.find(selectors.currentSlide); 3316 var $video = $currentSlide.find(selectors.productVideo); 3317 3318 this.settings.slickMainInitialized = true; 3319 3320 if (!$video.length) { 3321 return; 3322 } 3323 3324 this.initVideo($video); 3325 }, 3326 3327 thumbSlideInit: function(event, slick) { 3328 this.settings.slickThumbInitialized = true; 3329 }, 3330 3331 initVideo: function($video) { 3332 var videoType = $video.data('video-type'); 3333 var divId = $video.attr('id'); 3334 3335 if (videoType === 'mp4' && videos[divId].style === 'muted') { 3336 this.playMp4Video(divId); 3337 } 3338 3339 if (videoType === 'youtube') { 3340 if (youtubeReady && videos[divId].style === 'muted') { 3341 this.requestToPlayYoutubeVideo(divId); 3342 } 3343 } 3344 3345 if (videoType === 'vimeo') { 3346 if (vimeoReady) { 3347 this.playOrShowVimeo(divId); 3348 } else { 3349 $('body').on('vimeoReady' + this.settings.namespace, function() { 3350 this.playOrShowVimeo(divId); 3351 }.bind(this)) 3352 } 3353 } 3354 3355 // Hacky way to trigger resetting the slider layout in modals 3356 if (this.inModal) { 3357 this.resizeSlides(); 3358 } 3359 }, 3360 3361 stopVideo: function(id, type) { 3362 if (!id) { 3363 this.stopYoutubeVideo(); 3364 this.stopVimeoVideo(); 3365 this.stopMp4Video(); 3366 } 3367 3368 if (type === 'youtube') { 3369 this.stopYoutubeVideo(id); 3370 } 3371 3372 if (type === 'mp4') { 3373 this.stopMp4Video(id); 3374 } 3375 3376 if (type === 'vimeo') { 3377 this.stopVimeoVideo(id); 3378 } 3379 }, 3380 3381 playOrShowVimeo: function(id) { 3382 if (videos[id] && videos[id].style === 'muted') { 3383 this.autoplayVimeoVideo(id); 3384 } else if (videos[id] && videos[id].style === 'unmuted') { 3385 setParentAsLoaded($('#' + id)); 3386 } 3387 }, 3388 3389 getVideoType: function($video) { 3390 return $video.data('video-type'); 3391 }, 3392 3393 getVideoId: function($video) { 3394 return $video.attr('id'); 3395 }, 3396 3397 beforeSlideChange: function(event, slick, currentSlide, nextSlide) { 3398 var $slider = slick.$slider; 3399 var $currentSlide = $slider.find(selectors.currentSlide); 3400 var $prevVideo = $currentSlide.find('.product__video'); 3401 var $nextSlide = $slider.find('.slick-slide[data-slick-index="' + nextSlide + '"]'); 3402 var $nextVideo = $nextSlide.find('.product__video'); 3403 3404 // Pause any existing slide video 3405 if (currentSlide !== nextSlide && $prevVideo.length) { 3406 var prevVideoType = this.getVideoType($prevVideo); 3407 var prevVideoId = this.getVideoId($prevVideo); 3408 3409 if (prevVideoId) { 3410 this.stopVideo(prevVideoId, prevVideoType); 3411 } 3412 } 3413 3414 // Prep next slide video 3415 if ($nextVideo.length) { 3416 var nextVideoType = this.getVideoType($nextVideo); 3417 var nextVideoId = this.getVideoId($nextVideo); 3418 3419 // Prep Vimeo with a backup in case the API isn't ready 3420 if (nextVideoId && nextVideoType === 'vimeo') { 3421 if (vimeoReady) { 3422 if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') { 3423 this.autoplayVimeoVideo(nextVideoId); 3424 } 3425 } else { 3426 $('body').on('vimeoReady' + this.settings.namespace, function() { 3427 if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') { 3428 this.autoplayVimeoVideo(nextVideoId); 3429 } 3430 }.bind(this)) 3431 } 3432 } 3433 3434 // Prep YouTube with a backup in case API isn't ready 3435 if (nextVideoId && nextVideoType === 'youtube') { 3436 if (youtubeReady) { 3437 if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') { 3438 this.requestToPlayYoutubeVideo(nextVideoId, true); 3439 } 3440 } else { 3441 $('body').on('youTubeReady' + this.settings.namespace, function() { 3442 if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') { 3443 this.requestToPlayYoutubeVideo(nextVideoId, true); 3444 } 3445 }.bind(this)) 3446 } 3447 } 3448 3449 // Autoplay muted MP4 videos 3450 if (nextVideoId && videos[nextVideoId] && videos[nextVideoId].style === 'muted') { 3451 if (nextVideoType === 'mp4') { 3452 this.playMp4Video(nextVideoId); 3453 } 3454 } 3455 3456 // Set unmuted videos to loaded state 3457 if (nextVideoId && videos[nextVideoId] && videos[nextVideoId].style != 'muted') { 3458 setParentAsLoaded($('#' + nextVideoId)); 3459 } 3460 } 3461 }, 3462 3463 resizeSlides: function() { 3464 if (!this.settings.hasMultipleImages) { 3465 return; 3466 } 3467 3468 $(window).trigger('resize.slick'); 3469 setTimeout(function() { 3470 if (this.$mainSlider && this.settings.slickMainInitialized) { 3471 this.$mainSlider.slick('setPosition'); 3472 } 3473 if (this.$thumbSlider && this.settings.slickThumbInitialized) { 3474 this.$thumbSlider.slick('setPosition'); 3475 } 3476 }.bind(this), 500); // same timing as modal open transition 3477 }, 3478 3479 _slideIndex: function($el) { 3480 return $el.data('index'); 3481 }, 3482 3483 initQtySelector: function() { 3484 this.$container.find('.js-qty__wrapper').each(function() { 3485 new theme.QtySelector($(this), { 3486 namespace: '.product' 3487 }); 3488 }); 3489 }, 3490 3491 initAjaxProductForm: function() { 3492 if (theme.settings.cartType === 'drawer') { 3493 new theme.AjaxProduct($(this.selectors.formContainer)); 3494 } 3495 }, 3496 3497 openModalProduct: function() { 3498 if (!this.settings.modalInit) { 3499 var $formHolder = $(this.selectors.modalFormHolder); 3500 var url = $formHolder.data('url'); 3501 url = url + '?view=ajax'; 3502 3503 $formHolder.load(url + ' #AddToCartForm-' + this.sectionId, function() { 3504 this.formSetup(); 3505 if (Shopify.PaymentButton) { 3506 Shopify.PaymentButton.init(); 3507 } 3508 }.bind(this)); 3509 3510 this.productSetup(); 3511 this.loadModalContent(); 3512 this.createImageCarousels(); 3513 this.settings.modalInit = true; 3514 } 3515 3516 this.resizeSlides(); 3517 }, 3518 3519 closeModalProduct: function() { 3520 this.stopVideo(); 3521 $('body').off(this.settings.namespace); 3522 $(window).off(this.settings.namespace); 3523 }, 3524
3525 loadModalContent: function() { 3526 // Load videos if they exist 3527 var videoTypes = this.checkIfVideos(); 3528 3529 // Lazyload mp4 videos similar to images 3530 if (videoTypes && videoTypes.indexOf('mp4') > -1) { 3531 this.$modal.find('.product__video[data-video-type="mp4"]').each(function(i, video) { 3532 var $el = $(video); 3533 var src = $el.data('video-src'); 3534 var source = document.createElement('source'); 3535 source.setAttribute('src', src); 3536 $el.append(source); 3537 }.bind(this)); 3538 } 3539 }, 3540 3541 onUnload: function() { 3542 this.$container.off(this.settings.namespace); 3543 $('body').off(this.settings.namespace); 3544 this.destroyImageCarousels(); 3545 } 3546 }); 3547 3548 return Product; 3549 })(); 3550 3551 theme.Recommendations = (function() { 3552 3553 function Recommendations(container) { 3554 var $container = this.$container = $(container); 3555 var sectionId = this.sectionId = $container.attr('data-section-id'); 3556 3557 this.selectors = { 3558 recommendations: '#Recommendations-' + sectionId, 3559 placeholder: '.product-recommendations-placeholder', 3560 sectionClass: ' .product-recommendations', 3561 productResults: '.grid-product' 3562 }; 3563 3564 this.init(); 3565 } 3566 3567 Recommendations.prototype = $.extend({}, Recommendations.prototype, { 3568 init: function() { 3569 var $section = $(this.selectors.recommendations); 3570 3571 if (!$section.length || $section.data('enable') === false) { 3572 return; 3573 } 3574 3575 var $placeholder = $section.find(this.selectors.placeholder); 3576 var id = $section.data('product-id'); 3577 var limit = $section.data('limit'); 3578 3579 var url = '/recommendations/products?section_id=product-recommendations&limit='+ limit +'&product_id=' + id; 3580 3581 $placeholder.load(url + this.selectors.sectionClass, function(data) { 3582 theme.reinitProductGridItem($section); 3583 this.updateVariantInventory($section); 3584 3585 // If no results, hide the entire section 3586 if ($(data).find(this.selectors.sectionClass).find(this.selectors.productResults).length === 0) { 3587 $section.addClass('hide'); 3588 } 3589 }.bind(this)); 3590 }, 3591 3592 // Recommended products load via JS and don't add variant inventory to the 3593 // global variable that we later check. This function scrapes a data div 3594 // to get that info and manually add the values. 3595 updateVariantInventory: function($section) { 3596 window.inventories = window.inventories || {}; 3597 3598 $section.find('.product__inventory-data').each(function() { 3599 var $el = $(this); 3600 var sectionId = $el.data('section-id'); 3601 window.inventories[sectionId] = {}; 3602 $.each($el.data(), function(variant, qty) { 3603 window.inventories[sectionId][variant] = qty; 3604 }) 3605 }); 3606 } 3607 }); 3608 3609 return Recommendations; 3610 })(); 3611 3612 theme.Collection = (function() { 3613 var isAnimating = false; 3614 3615 var classes = { 3616 tags: '.tags', 3617 activeTag: 'tag--active' 3618 }; 3619 3620 function Collection(container) { 3621 this.container = container; 3622 this.sectionId = $(container).attr('data-section-id'); 3623 this.namespace = '.collection-' + this.sectionId; 3624 3625 var $heroImageContainer = this.$heroContainer = $(container).find('.collection-hero'); 3626 if ($heroImageContainer.length) { 3627 this.checkIfNeedReload(); 3628 theme.loadImageSection($heroImageContainer); 3629 } else if (theme.settings.overlayHeader) { 3630 theme.headerNav.disableOverlayHeader(); 3631 } 3632 3633 // Ajax pagination 3634 $(window).on('popstate', function(state) { 3635 if (state) { 3636 this.getNewCollectionContent(location.href); 3637 } 3638 }.bind(this)); 3639 3640 this.init(); 3641 } 3642 3643 Collection.prototype = $.extend({}, Collection.prototype, { 3644 init: function() { 3645 // init is called on load and when tags are selected 3646 this.$container = $(this.container); 3647 this.sectionId = this.$container.attr('data-section-id'); 3648 3649 this.sortBy(); 3650 this.sortTags(); 3651 this.initTagAjax(); 3652 }, 3653 3654 initTagAjax: function() {
3655 this.$container.on('click' + this.namespace, '.tags a', function(evt) { 3656 var $el = $(evt.currentTarget); 3657 if ($el.hasClass('no-ajax')) { 3658 return; 3659 } 3660 3661 evt.preventDefault(); 3662 3663 if (isAnimating) { 3664 return; 3665 } 3666 3667 isAnimating = true; 3668 3669 var newUrl = $el.attr('href'); 3670 $(classes.tags).find('.' + classes.activeTag).removeClass(classes.activeTag); 3671 $el.parent().addClass(classes.activeTag); 3672 history.pushState({}, '', newUrl); 3673 $('.grid-product').addClass('unload'); 3674 this.getNewCollectionContent(newUrl); 3675 }.bind(this)); 3676 }, 3677 3678 getNewCollectionContent: function(url) { 3679 url = url + '?view=ajax'; 3680 $('#CollectionAjaxResult').load(url + ' #CollectionAjaxContent', function() { 3681 isAnimating = false; 3682 this.reInit(); 3683 }.bind(this)); 3684 }, 3685 3686 sortBy: function() { 3687 var $sortBy = $('#SortBy'); 3688 3689 if (!$sortBy.length) { 3690 return; 3691 } 3692 3693 $sortBy.on('change', function() { 3694 location.href = '?sort_by=' + $(this).val(); 3695 }); 3696 }, 3697 3698 sortTags: function() { 3699 var $sortTags = $('#SortTags'); 3700 3701 if (!$sortTags.length) { 3702 return; 3703 } 3704 3705 $sortTags.on('change', function() { 3706 location.href = $(this).val(); 3707 }); 3708 }, 3709 3710 reInit: function() { 3711 // Reload collection template (it can be anywhere in the sections.instance array) 3712 for (var i = 0; i < sections.instances.length; i++) { 3713 var instance = sections.instances[i]; 3714 if (instance['type'] === 'collection-template') { 3715 instance.forceReload(); 3716 } 3717 } 3718 3719 theme.reinitProductGridItem(); 3720 }, 3721 3722 forceReload: function() { 3723 this.onUnload(); 3724 this.init(); 3725 }, 3726 3727 // A liquid variable in the header needs a full page refresh 3728 // if the collection header hero image setting is enabled 3729 // and the header is set to sticky. Only necessary in the editor. 3730 checkIfNeedReload: function() { 3731 if (!Shopify.designMode) { 3732 return; 3733 } 3734 3735 if (theme.settings.overlayHeader) { 3736 if (!$('.header-wrapper').hasClass('header-wrapper--overlay')) { 3737 location.reload(); 3738 } 3739 } 3740 }, 3741 3742 onUnload: function() { 3743 $(window).off(this.namespace); 3744 this.$container.off(this.namespace); 3745 } 3746 3747 }); 3748 3749 return Collection; 3750 })(); 3751 3752 theme.CollectionSwitcher = (function() { 3753 var selectors = { 3754 trigger: '.collection-switcher__trigger', 3755 collection: '.collection-switcher__collection', 3756 slider: '.collection-switcher__collection-grid' 3757 }; 3758 3759 function CollectionSwitcher(container) { 3760 this.$container = $(container); 3761 this.sectionId = this.$container.attr('data-section-id'); 3762 3763 this.config = { 3764 perRow: this.$container.attr('data-per-row'), 3765 perRowMobile: 2 // fixed always 3766 }; 3767 3768 this.sliderArgs = { 3769 arrows: true, 3770 infinite: false, 3771 slidesToShow: this.config.perRow, 3772 slidesToScroll: this.config.perRow 3773 }; 3774 3775 this.$activeSlider = null; 3776 3777 this.init(); 3778 } 3779 3780 CollectionSwitcher.prototype = $.extend({}, CollectionSwitcher.prototype, { 3781 init: function() { 3782 this.$triggers = this.$container.find(selectors.trigger); 3783 this.$collections = this.$container.find(selectors.collection); 3784 3785 var $firstSlider = this.$container.find(selectors.slider).first(); 3786 if ($firstSlider.length) { 3787 this.initSlider($firstSlider); 3788 3789 $('body').on('matchSmall unmatchSmall', function() { 3790 this.initSlider(); 3791 }.bind(this)); 3792 } 3793 3794 this.$triggers.on('click', this.switch.bind(this)); 3795 }, 3796 3797 initSlider: function($slider) { 3798 if (this.$activeSlider) { 3799 this.$activeSlider 3800 .off('afterChange') 3801 .slick('unslick'); 3802 } 3803 3804 if (!$slider) { 3805 $slider = this.$activeSlider; 3806 } 3807 3808 if (!$slider.length) { 3809 return; 3810 } 3811 3812 // Mobile-specific per-row setting 3813 if (theme.config.bpSmall) { 3814 this.sliderArgs.slidesToShow = this.config.perRowMobile; 3815 this.sliderArgs.slidesToScroll = this.config.perRowMobile; 3816 } else { 3817 this.sliderArgs.slidesToShow = parseInt(this.config.perRow); 3818 this.sliderArgs.slidesToScroll = parseInt(this.config.perRow); 3819 } 3820 3821 this.$activeSlider = $slider.slick(this.sliderArgs); 3822 3823 if (AOS) { 3824 AOS.refresh(); 3825 } 3826 }, 3827 3828 switch: function(evt) { 3829 evt.preventDefault(); 3830 3831 this.$triggers.removeClass('is-active'); 3832 3833 var containerId = $(evt.currentTarget).addClass('is-active').attr('aria-controls'); 3834 var $container = $('#' + containerId); 3835 var $newSlider = $container.find(selectors.slider); 3836 3837 // Hide collections, show new one 3838 this.$collections.addClass('hide'); 3839 $container.removeClass('hide'); 3840 3841 this.initSlider($newSlider); 3842 }, 3843 3844 onUnload: function() { 3845 3846 } 3847 3848 }); 3849 3850 return CollectionSwitcher; 3851 })(); 3852 3853 theme.HeaderSection = (function() { 3854 3855 var selectors = { 3856 drawer: '#NavDrawer', 3857 mobileSubNavToggle: '.mobile-nav__toggle-btn', 3858 hasSublist: '.mobile-nav__has-sublist' 3859 }; 3860 3861 var classes = { 3862 navExpanded: 'mobile-nav--expanded' 3863 }; 3864 3865 function Header(container) { 3866 var $container = this.$container = $(container); 3867 var sectionId = this.sectionId = $container.attr('data-section-id'); 3868 3869 // Reload any slideshow if when the header is reloaded to make sure the 3870 // sticky header works as expected (it can be anywhere in the sections.instance array) 3871 for (var i = 0; i < sections.instances.length; i++) { 3872 var instance = sections.instances[i]; 3873 if (instance['type'] === 'slideshow-section') { 3874 instance.forceReload(); 3875 } 3876 } 3877 3878 theme.currencySwitcher.init() 3879 this.initDrawers(); 3880 theme.headerNav.init();
3881 theme.announcementBar.init(); 3882 } 3883 3884 Header.prototype = $.extend({}, Header.prototype, { 3885 initDrawers: function() { 3886 theme.NavDrawer = new theme.Drawers('NavDrawer', 'nav'); 3887 if (theme.settings.cartType === 'drawer') { 3888 new theme.CartDrawer(); 3889 } 3890 3891 this.drawerMenuButtons(); 3892 }, 3893 3894 drawerMenuButtons: function() { 3895 $(selectors.drawer).find('.js-drawer-close').on('click', function(evt){ 3896 evt.preventDefault(); 3897 theme.NavDrawer.close(); 3898 }); 3899 3900 var $mobileSubNavToggle = $(selectors.mobileSubNavToggle); 3901 3902 $mobileSubNavToggle.attr('aria-expanded', 'false'); 3903 $mobileSubNavToggle.each(function (i, el) { 3904 var $el = $(el); 3905 $el.attr('aria-controls', $el.attr('data-aria-controls')); 3906 }); 3907 3908 $mobileSubNavToggle.on('click', function() { 3909 var $el = $(this); 3910 var currentlyExpanded = $el.attr('aria-expanded'); 3911 var toggleState = false; 3912 3913 // Updated aria-expanded value based on state pre-click 3914 if (currentlyExpanded === 'true') { 3915 $el.attr('aria-expanded', 'false'); 3916 } else { 3917 $el.attr('aria-expanded', 'true'); 3918 toggleState = true; 3919 } 3920 3921 // Toggle class that expands/collapses sublist 3922 $el.closest(selectors.hasSublist).toggleClass(classes.navExpanded, toggleState); 3923 }); 3924 }, 3925 3926 onUnload: function() { 3927 theme.NavDrawer.close(); 3928 theme.headerNav.unload(); 3929 theme.announcementBar.unload(); 3930 } 3931 }); 3932 3933 return Header; 3934 3935 })(); 3936 3937 theme.FeaturedContentSection = (function() { 3938 3939 function FeaturedContent() { 3940 $('.rte').find('a:not(:has(img))').addClass('text-link'); 3941 } 3942 3943 return FeaturedContent; 3944 })(); 3945 3946 theme.slideshows = {}; 3947 3948 theme.SlideshowSection = (function() { 3949 3950 function SlideshowSection(container) { 3951 var $container = this.$container = $(container); 3952 var $section = $container.parent(); 3953 var sectionId = $container.attr('data-section-id'); 3954 var slideshow = this.slideshow = '#Slideshow-' + sectionId; 3955 3956 var $imageContainer = $(container).find('.hero'); 3957 if ($imageContainer.length) { 3958 theme.loadImageSection($imageContainer); 3959 } 3960 3961 this.init(); 3962 } 3963 3964 SlideshowSection.prototype = $.extend({}, SlideshowSection.prototype, { 3965 init: function() { 3966 var args = { 3967 arrows: $(this.slideshow).data('arrows'), 3968 dots: $(this.slideshow).data('dots') 3969 }; 3970 3971 theme.slideshows[this.slideshow] = new theme.Slideshow(this.slideshow, args); 3972 }, 3973 3974 forceReload: function() { 3975 this.onUnload(); 3976 this.init(); 3977 }, 3978 3979 onUnload: function() { 3980 theme.slideshows[this.slideshow].destroy(); 3981 delete theme.slideshows[this.slideshow]; 3982 }, 3983 3984 onSelect: function() { 3985 $(this.slideshow).slick('slickPause'); 3986 }, 3987 3988 onDeselect: function() { 3989 $(this.slideshow).slick('slickPlay'); 3990 }, 3991 3992 onBlockSelect: function(evt) { 3993 var $slideshow = $(this.slideshow); 3994 3995 // Ignore the cloned version 3996 var $slide = $('.slideshow__slide--' + evt.detail.blockId + ':not(.slick-cloned)'); 3997 var slideIndex = $slide.data('slick-index'); 3998 3999 // Go to selected slide, pause autoplay 4000 $slideshow.slick('slickGoTo', slideIndex).slick('slickPause'); 4001 }, 4002 4003 onBlockDeselect: function() { 4004 $(this.slideshow).slick('slickPlay'); 4005 } 4006 }); 4007 4008 return SlideshowSection; 4009 })(); 4010 4011 theme.VideoSection = (function() { 4012 var youtubeReady; 4013 var videos = []; 4014 var youtubePlayers = []; 4015 var youtubeVideoOptions = { 4016 width: 1280, 4017 height: 720, 4018 playerVars: { 4019 autohide: 0, 4020 branding: 0, 4021 cc_load_policy: 0, 4022 controls: 0, 4023 fs: 0, 4024 iv_load_policy: 3, 4025 modestbranding: 1, 4026 playsinline: 1, 4027 quality: 'hd720', 4028 rel: 0, 4029 showinfo: 0, 4030 wmode: 'opaque' 4031 }, 4032 events: { 4033 onReady: onVideoPlayerReady, 4034 onStateChange: onVideoStateChange 4035 } 4036 }; 4037 4038 var vimeoReady = false;
4039 var vimeoVideoOptions = { 4040 byline: false, 4041 title: false, 4042 portrait: false, 4043 loop: true 4044 }; 4045 4046 var selectors = { 4047 videoParent: '.video-parent-section' 4048 }; 4049 4050 var classes = { 4051 loading: 'loading', 4052 loaded: 'loaded', 4053 interactable: 'video-interactable' 4054 }; 4055 4056 function videoSection(container) { 4057 var $container = this.$container = $(container); 4058 var sectionId = this.sectionId = $container.attr('data-section-id'); 4059 var youtubePlayerId = this.youtubePlayerId = 'YouTubeVideo-' + this.sectionId; 4060 this.namespace = '.' + youtubePlayerId; 4061 var vimeoPlayerId = this.vimeoPlayerId = 'Vimeo-' + this.sectionId; 4062 var $vimeoTrigger = this.$vimeoTrigger = $('#VimeoTrigger-' + this.sectionId); 4063 var mp4Video = 'Mp4Video-' + this.sectionId; 4064 4065 var $youtubeDiv = $('#' + youtubePlayerId); 4066 var $vimeoDiv = $('#' + vimeoPlayerId); 4067 var $mp4Div = $('#' + mp4Video); 4068 4069 this.vimeoPlayer = []; 4070 4071 if ($youtubeDiv.length) { 4072 this.youtubeVideoId = $youtubeDiv.data('video-id'); 4073 this.initYoutubeVideo(); 4074 } 4075 4076 if ($vimeoDiv.length) { 4077 this.vimeoVideoId = $vimeoDiv.data('video-id'); 4078 this.initVimeoVideo(); 4079 } 4080 4081 if ($mp4Div.length) { 4082 setParentAsLoaded($mp4Div); 4083 4084 startMp4Playback(mp4Video).then(function() { 4085 // Video played as expected, don't do anything 4086 }).catch(function(error) { 4087 // Video cannot be played with autoplay, so let 4088 // user interact with video element itself 4089 setVideoToBeInteractedWith($mp4Div); 4090 }) 4091 } 4092 } 4093 4094 function startMp4Playback(mp4Video) { 4095 return document.querySelector('#' + mp4Video).play(); 4096 } 4097 4098 function onVideoPlayerReady(evt) { 4099 var $player = $(evt.target.a); 4100 var playerId = $player.attr('id'); 4101 youtubePlayers[playerId] = evt.target; // update stored player 4102 var player = youtubePlayers[playerId]; 4103 4104 setParentAsLoading($player); 4105 4106 youtubePlayers[playerId].mute(); 4107 4108 // Remove from tabindex because YouTube iframes are annoying and you can focus 4109 // on the YouTube logo and it breaks 4110 $player.attr('tabindex', '-1'); 4111 4112 // Add out of view pausing 4113 videoVisibilityCheck(playerId); 4114 $(window).on('scroll.' + playerId, {id: playerId}, $.throttle(150, videoVisibilityCheck)); 4115 } 4116 4117 function videoVisibilityCheck(id) { 4118 var playerId; 4119 4120 if (typeof id === 'string') { 4121 playerId = id; 4122 } else { 4123 // Data comes in as part of the scroll event 4124 playerId = id.data.id; 4125 } 4126 4127 if (theme.isElementVisible($('#' + playerId))) { 4128 playVisibleVideo(playerId); 4129 } else { 4130 pauseHiddenVideo(playerId); 4131 } 4132 } 4133 4134 function playVisibleVideo(id) { 4135 if (youtubePlayers[id] && typeof youtubePlayers[id].playVideo === 'function') { 4136 youtubePlayers[id].playVideo(); 4137 } 4138 } 4139 4140 function pauseHiddenVideo(id) { 4141 if (youtubePlayers[id] && typeof youtubePlayers[id].pauseVideo === 'function') { 4142 youtubePlayers[id].pauseVideo(); 4143 } 4144 } 4145 4146 function onVideoStateChange(evt) { 4147 var $player = $(evt.target.a); 4148 var playerId = $player.attr('id'); 4149 var player = youtubePlayers[playerId]; 4150 4151 switch (evt.data) { 4152 case -1: // unstarted 4153 // Handle low power state on iOS by checking if 4154 // video is reset to unplayed after attempting to buffer 4155 if (videos[playerId].attemptedToPlay) { 4156 setParentAsLoaded($player); 4157 setVideoToBeInteractedWith($player); 4158 } 4159 break; 4160 case 0: // ended 4161 player.playVideo(); 4162 break; 4163 case 1: // playing 4164 setParentAsLoaded($player); 4165 break; 4166 case 3: // buffering 4167 videos[playerId].attemptedToPlay = true; 4168 break; 4169 } 4170 } 4171 4172 function setParentAsLoading($el) { 4173 $el 4174 .closest(selectors.videoParent)
4175 .addClass(classes.loading); 4176 } 4177 4178 function setParentAsLoaded($el) { 4179 $el 4180 .closest(selectors.videoParent) 4181 .removeClass(classes.loading) 4182 .addClass(classes.loaded); 4183 } 4184 4185 function setVideoToBeInteractedWith($el) { 4186 $el 4187 .closest(selectors.videoParent) 4188 .addClass(classes.interactable); 4189 } 4190 4191 videoSection.prototype = $.extend({}, videoSection.prototype, { 4192 initYoutubeVideo: function() { 4193 videos[this.youtubePlayerId] = { 4194 id: this.youtubePlayerId, 4195 videoId: this.youtubeVideoId, 4196 type: 'youtube', 4197 attemptedToPlay: false 4198 }; 4199 4200 if (!youtubeReady) { 4201 window.loadYouTube(); 4202 $('body').on('youTubeReady' + this.namespace, this.loadYoutubeVideo.bind(this)); 4203 } else { 4204 this.loadYoutubeVideo(); 4205 } 4206 }, 4207 4208 loadYoutubeVideo: function() { 4209 var args = $.extend({}, youtubeVideoOptions, videos[this.youtubePlayerId]); 4210 args.playerVars.controls = 0; 4211 youtubePlayers[this.youtubePlayerId] = new YT.Player(this.youtubePlayerId, args); 4212 4213 youtubeReady = true; 4214 }, 4215 4216 initVimeoVideo: function() { 4217 videos[this.vimeoPlayerId] = { 4218 divId: this.vimeoPlayerId, 4219 id: this.vimeoVideoId, 4220 type: 'vimeo' 4221 }; 4222 4223 var $player = $('#' + this.vimeoPlayerId); 4224 setParentAsLoading($player); 4225 4226 // Button to play video on mobile 4227 this.$vimeoTrigger.on('click', + this.namespace, function(evt) { 4228 // $(evt.currentTarget).addClass('hide'); 4229 this.requestToPlayVimeoVideo(this.vimeoPlayerId); 4230 }.bind(this)); 4231 4232 if (!vimeoReady) { 4233 window.loadVimeo(); 4234 $('body').on('vimeoReady' + this.namespace, this.loadVimeoVideo.bind(this)); 4235 } else { 4236 this.loadVimeoVideo(); 4237 } 4238 }, 4239 4240 loadVimeoVideo: function() { 4241 var args = $.extend({}, vimeoVideoOptions, videos[this.vimeoPlayerId]); 4242 this.vimeoPlayer[this.vimeoPlayerId] = new Vimeo.Player(videos[this.vimeoPlayerId].divId, args); 4243 4244 vimeoReady = true; 4245 4246 // Only autoplay on larger screens 4247 if (!theme.config.bpSmall) { 4248 this.requestToPlayVimeoVideo(this.vimeoPlayerId); 4249 } else { 4250 var $player = $('#' + this.vimeoPlayerId); 4251 setParentAsLoaded($player); 4252 } 4253 }, 4254 4255 requestToPlayVimeoVideo: function(id) { 4256 // The slider may initialize and attempt to play the video before 4257 // the API is even ready, because it sucks. 4258 4259 if (!vimeoReady) { 4260 // Wait for the trigger, then play it 4261 $('body').on('vimeoReady' + this.namespace, function() { 4262 this.playVimeoVideo(id); 4263 }.bind(this)) 4264 return; 4265 } 4266 4267 this.playVimeoVideo(id); 4268 }, 4269 4270 playVimeoVideo: function(id) { 4271 this.vimeoPlayer[id].play(); 4272 this.vimeoPlayer[id].setVolume(0); 4273 4274 var $player = $('#' + id); 4275 setParentAsLoaded($player); 4276 }, 4277 4278 onUnload: function(evt) { 4279 var sectionId = evt.target.id.replace('shopify-section-', ''); 4280 var playerId = 'YouTubeVideo-' + sectionId; 4281 youtubePlayers[playerId].destroy(); 4282 $(window).off('scroll' + this.namespace); 4283 $('body').off('vimeoReady' + this.namespace); 4284 } 4285 }); 4286 4287 return videoSection; 4288 })(); 4289 4290 theme.Testimonials = (function() { 4291 var slideCount = 0; 4292 var defaults = { 4293 accessibility: true, 4294 arrows: false, 4295 dots: true, 4296 autoplay: false, 4297 touchThreshold: 20, 4298 slidesToShow: 3, 4299 slidesToScroll: 3 4300 }; 4301 4302 function Testimonials(container) { 4303 var $container = this.$container = $(container); 4304 var sectionId = $container.attr('data-section-id'); 4305 var wrapper = this.wrapper = '.testimonials-wrapper'; 4306 var slider = this.slider = '#Testimonials-' + sectionId; 4307 var $slider = $(slider); 4308 4309 this.sliderActive = false; 4310 var mobileOptions = $.extend({}, defaults, { 4311 slidesToShow: 1, 4312 slidesToScroll: 1, 4313 adaptiveHeight: true 4314 }); 4315 4316 slideCount = $slider.data('count'); 4317 4318 // Override slidesToShow/Scroll if there are not enough blocks 4319 if (slideCount < defaults.slidesToShow) { 4320 defaults.slidesToShow = slideCount; 4321 defaults.slidesToScroll = slideCount; 4322 } 4323 4324 $slider.on('init', this.a11y.bind(this)); 4325 4326 if (theme.config.bpSmall) { 4327 this.init($slider, mobileOptions); 4328 } else { 4329 this.init($slider, defaults); 4330 } 4331 4332 $('body').on('matchSmall', function() { 4333 this.init($slider, mobileOptions); 4334 }.bind(this)); 4335 4336 $('body').on('unmatchSmall', function() { 4337 this.init($slider, defaults); 4338 }.bind(this)); 4339 } 4340 4341 Testimonials.prototype = $.extend({}, Testimonials.prototype, { 4342 onUnload: function() { 4343 $(this.slider, this.wrapper).slick('unslick'); 4344 }, 4345 4346 onBlockSelect: function(evt) { 4347 // Ignore the cloned version 4348 var $slide = $('.testimonials-slide--' + evt.detail.blockId + ':not(.slick-cloned)'); 4349 var slideIndex = $slide.data('slick-index'); 4350 4351 // Go to selected slide, pause autoplay 4352 $(this.slider, this.wrapper).slick('slickGoTo', slideIndex); 4353 }, 4354 4355 init: function(obj, args) { 4356 if (this.sliderActive) { 4357 obj.slick('unslick'); 4358 this.sliderActive = false;
4359 } 4360 4361 obj.slick(args); 4362 this.sliderActive = true; 4363 4364 if (AOS) { 4365 AOS.refresh(); 4366 } 4367 }, 4368 4369 a11y: function(event, obj) { 4370 var $list = obj.$list; 4371 var $wrapper = $(this.wrapper, this.$container); 4372 4373 // Remove default Slick aria-live attr until slider is focused 4374 $list.removeAttr('aria-live'); 4375 4376 // When an element in the slider is focused set aria-live 4377 $wrapper.on('focusin', function(evt) { 4378 if ($wrapper.has(evt.target).length) { 4379 $list.attr('aria-live', 'polite'); 4380 } 4381 }); 4382 4383 // Remove aria-live 4384 $wrapper.on('focusout', function(evt) { 4385 if ($wrapper.has(evt.target).length) { 4386 $list.removeAttr('aria-live'); 4387 } 4388 }); 4389 } 4390 }); 4391 4392 return Testimonials; 4393 })(); 4394 4395 theme.Instagram = (function() { 4396 var isInit = false; 4397 4398 function Instagram(container) { 4399 var $container = this.$container = $(container); 4400 var sectionId = $container.attr('data-section-id'); 4401 this.namespace = '.instagram-' + sectionId; 4402 this.$target = $('#Instafeed-' + sectionId); 4403 4404 if (!this.$target.length) { 4405 return; 4406 } 4407 4408 this.checkVisibility(); 4409 $(window).on('scroll' + this.namespace, $.throttle(100, this.checkVisibility.bind(this))); 4410 } 4411 4412 Instagram.prototype = $.extend({}, Instagram.prototype, { 4413 checkVisibility: function() { 4414 if (isInit) { 4415 $(window).off('scroll' + this.namespace); 4416 return; 4417 } 4418 4419 if (theme.isElementVisible(this.$container)) { 4420 this.init(); 4421 } 4422 }, 4423 4424 init: function() { 4425 isInit = true; 4426 4427 var userId = this.$target.data('user-id'); 4428 var clientId = this.$target.data('client-id'); 4429 var count = parseInt(this.$target.data('count')); 4430 4431 // Ask for 2 more images than we'll actually show because 4432 // Instagram sometimes doesn't send enough 4433 var feed = this.feed = new Instafeed({ 4434 target: this.$target[0], 4435 accessToken: clientId, 4436 get: 'user', 4437 userId: userId, 4438 limit: count + 2, 4439 template: '<div class="grid__item small--one-half medium-up--one-fifth"><div class="image-wrap"><a href="{{link}}" target="_blank" style="background-image: url({{image}}); display: block; padding-bottom: 100%; background-size: cover; background-position: center;"></a></div></div>', 4440 resolution: 'standard_resolution' 4441 }); 4442 4443 feed.run(); 4444 4445 this.$target.addClass('instagram-holder--active'); 4446 } 4447 }); 4448 4449 return Instagram; 4450 })(); 4451 4452 4453 theme.NewsletterPopup = (function() { 4454 function NewsletterPopup(container) { 4455 var $container = this.$container = $(container); 4456 var sectionId = $container.attr('data-section-id'); 4457 this.cookieName = 'newsletter-' + sectionId; 4458 4459 if (!$container.length) { 4460 return; 4461 } 4462 4463 // Prevent popup on Shopify robot challenge page 4464 if (window.location.pathname === '/challenge') { 4465 return; 4466 } 4467 4468 this.data = { 4469 secondsBeforeShow: $container.data('delay-seconds'), 4470 daysBeforeReappear: $container.data('delay-days'), 4471 cookie: Cookies.get(this.cookieName), 4472 testMode: $container.data('test-mode') 4473 }; 4474 4475 this.modal = new theme.Modals('NewsletterPopup-' + sectionId, 'newsletter-popup-modal'); 4476 4477 // Open modal if errors or success message exist 4478 if ($container.find('.errors').length || $container.find('.note--success').length) { 4479 this.modal.open(); 4480 } 4481 4482 // Set cookie as opened if success message 4483 if ($container.find('.note--success').length) { 4484 this.closePopup(true); 4485 return; 4486 } 4487 4488 $('body').on('modalClose.' + $container.attr('id'), this.closePopup.bind(this)); 4489 4490 if (!this.data.cookie || this.data.testMode) { 4491 this.initPopupDelay(); 4492 } 4493 } 4494 4495 NewsletterPopup.prototype = $.extend({}, NewsletterPopup.prototype, { 4496 initPopupDelay: function() { 4497 setTimeout(function() { 4498 this.modal.open(); 4499 }.bind(this), this.data.secondsBeforeShow * 1000); 4500 }, 4501 4502 closePopup: function(success) { 4503 // Remove a cookie in case it was set in test mode 4504 if (this.data.testMode) { 4505 Cookies.remove(this.cookieName, { path: '/' }); 4506 return; 4507 } 4508 4509 var expiry = success ? 200 : this.data.daysBeforeReappear; 4510 4511 Cookies.set(this.cookieName, 'opened', { path: '/', expires: expiry }); 4512 }, 4513 4514 onLoad: function() { 4515 this.modal.open(); 4516 }, 4517 4518 onSelect: function() { 4519 this.modal.open(); 4520 }, 4521 4522 onDeselect: function() { 4523 this.modal.close(); 4524 }, 4525 4526 onUnload: function() {} 4527 }); 4528 4529 return NewsletterPopup; 4530 })(); 4531 4532 theme.FadingImages = (function() { 4533 4534 var classes = { 4535 activeImage: 'active-image', 4536 finishedImage: 'finished-image', 4537 activeTitles: 'active-titles', 4538 finishedTitles: 'finished-titles', 4539 compensation: 'compensation' 4540 }; 4541 4542 function FadingImages(container) { 4543 var $container = this.$container = $(container); 4544 var sectionId = $container.attr('data-section-id'); 4545 var namespace = this.namespace = '.fading-images-' + sectionId; 4546 4547 if (!$container.length) { 4548 return; 4549 } 4550 4551 var $imageContainer = $container.find('.fading-images'); 4552 theme.loadImageSection($imageContainer); 4553 4554 this.data = { 4555 isInit: false, 4556 interval: $container.data('interval'), 4557 block_count: $container.data('count'), 4558 finish_interval: 1000, 4559 timer_offset: 400, 4560 active_image: 1, 4561 active_title: 1, 4562 removed_compensation: false 4563 }; 4564 4565 this.selectors = { 4566 $allTitles: $container.find('.fading-images-overlay__titles') 4567 }; 4568 4569 this.checkVisibility(); 4570 $(window).on('scroll' + this.namespace, $.throttle(100, this.checkVisibility.bind(this))); 4571 } 4572 4573 FadingImages.prototype = $.extend({}, FadingImages.prototype, { 4574 checkVisibility: function() { 4575 if (this.data.isInit) { 4576 $(window).off('scroll' + this.namespace); 4577 return; 4578 } 4579 4580 if (theme.isElementVisible(this.$container)) { 4581 this.startImages(); 4582 this.startTitles(); 4583 this.data.isInit = true; 4584 } 4585 }, 4586 4587 nextImage: function() { 4588 var $container = this.$container; 4589 4590 if (!this.data.removed_compensation) {
4591 $container.find('.fading-images__item[data-slide-index=' + this.data.active_image + ']').removeClass(classes.compensation); 4592 this.data.removed_compensation = true; 4593 } 4594 4595 $container 4596 .find('.fading-images__item[data-slide-index=' + this.data.active_image + ']') 4597 .removeClass(classes.activeImage) 4598 .addClass(classes.finishedImage); 4599 4600 var target_image = this.data.active_image; 4601 window.setTimeout(function() { 4602 $container.find('.fading-images__item[data-slide-index=' + target_image + ']').removeClass(classes.finishedImage); 4603 }, this.data.finish_interval); 4604 4605 this.data.active_image++; 4606 if (this.data.active_image > this.data.block_count) { 4607 this.data.active_image = 1; 4608 } 4609 4610 $container.find('.fading-images__item[data-slide-index=' + this.data.active_image + ']').addClass(classes.activeImage); 4611 }, 4612 4613 nextTitle: function() { 4614 var $container = this.$container; 4615 var $allTitles = this.selectors.$allTitles; 4616 4617 this.selectors.$allTitles.removeClass(classes.activeTitles).addClass(classe
4617s.finishedTitles); 4618 4619 this.data.active_title++; 4620 if (this.data.active_title > this.data.block_count) { 4621 this.data.active_title = 1; 4622 } 4623 4624 var target_title = this.data.active_title; 4625 window.setTimeout(function() { 4626 var newText1 = $container.find('.fading-images__item[data-slide-index=' + target_title + ']').attr('data-slide-title1'); 4627 var newText2 = $container.find('.fading-images__item[data-slide-index=' + target_title + ']').attr('data-slide-title2'); 4628 $container.find('.fading-images-overlay__title--1').text(newText1); 4629 $container.find('.fading-images-overlay__title--2').text(newText2); 4630 $allTitles.removeClass(classes.finishedTitles).addClass(classes.activeTitles); 4631 }, this.data.finish_interval - 200); 4632 }, 4633 4634 startImages: function() { 4635 // Prep and show first image 4636 this.$container.find('.fading-images__item[data-slide-index=' + this.data.active_image + ']').addClass(classes.activeImage).addClass(classe
4636s.compensation); 4637 4638 // Begin timer 4639 var o = this; 4640 window.setTimeout(function() { 4641 var fading_images_timer = window.setInterval(o.nextImage.bind(o), o.data.interval); 4642 }, this.data.timer_offset); 4643 }, 4644 4645 startTitles: function() { 4646 var $container = this.$container; 4647 var $allTitles = this.selectors.$allTitles; 4648 // Prep and show first titles 4649 var target_title = this.data.active_title; 4650 window.setTimeout(function() { 4651 var newText1 = $container.find('.fading-images__item[data-slide-index=' + target_title + ']').attr('data-slide-title1'); 4652 var newText2 = $container.find('.fading-images__item[data-slide-index=' + target_title + ']').attr('data-slide-title2'); 4653 $container.find('.fading-images-overlay__title--1').text(newText1); 4654 $container.find('.fading-images-overlay__title--2').text(newText2); 4655 $allTitles.addClass(classes.activeTitles); 4656 }, 750); 4657 4658 // Begin timer 4659 var fading_titles_timer = window.setInterval(this.nextTitle.bind(this), this.data.interval); 4660 }, 4661 4662 onLoad: function() { 4663 }, 4664 4665 onSelect: function() { 4666 }, 4667 4668 onDeselect: function() { 4669 }, 4670 4671 onUnload: function() { 4672 } 4673 }); 4674 4675 return FadingImages; 4676 })(); 4677 4678 theme.Maps = (function() { 4679 var config = { 4680 zoom: 14 4681 }; 4682 var apiStatus = null; 4683 var mapsToLoad = []; 4684 4685 var errors = { 4686 addressNoResults: theme.strings.addressNoResults, 4687 addressQueryLimit: theme.strings.addressQueryLimit, 4688 addressError: theme.strings.addressError, 4689 authError: theme.strings.authError 4690 }; 4691 4692 var selectors = { 4693 section: '[data-section-type="map"]', 4694 map: '[data-map]', 4695 mapOverlay: '[data-map-overlay]' 4696 }; 4697 4698 var classes = { 4699 mapError: 'map-section--load-error', 4700 errorMsg: 'map-section__error errors text-center' 4701 }; 4702 4703 // Global function called by Google on auth errors. 4704 // Show an auto error message on all map instances. 4705 window.gm_authFailure = function() { 4706 if (!Shopify.designMode) { 4707 return; 4708 } 4709
4710 $(selectors.section).addClass(classes.mapError); 4711 $(selectors.map).remove(); 4712 $(selectors.mapOverlay).after( 4713 '<div class="' + 4714 classes.errorMsg + 4715 '">' + 4716 theme.strings.authError + 4717 '</div>' 4718 ); 4719 }; 4720 4721 function Map(container) { 4722 this.$container = $(container); 4723 this.sectionId = this.$container.attr('data-section-id'); 4724 this.namespace = '.map-' + this.sectionId; 4725 this.$map = this.$container.find(selectors.map); 4726 this.key = this.$map.data('api-key'); 4727 4728 if (!this.key) { 4729 return; 4730 } 4731 4732 // Lazyload API 4733 this.checkVisibility(); 4734 $(window).on('scroll' + this.namespace, $.throttle(50, this.checkVisibility.bind(this))); 4735 } 4736 4737 function initAllMaps() { 4738 // API has loaded, load all Map instances in queue 4739 $.each(mapsToLoad, function(index, instance) { 4740 instance.createMap(); 4741 }); 4742 } 4743 4744 function geolocate($map) { 4745 var deferred = $.Deferred(); 4746 var geocoder = new google.maps.Geocoder(); 4747 var address = $map.data('address-setting'); 4748 4749 geocoder.geocode({ address: address }, function(results, status) { 4750 if (status !== google.maps.GeocoderStatus.OK) { 4751 deferred.reject(status); 4752 } 4753 4754 deferred.resolve(results); 4755 }); 4756 4757 return deferred; 4758 } 4759 4760 Map.prototype = $.extend({}, Map.prototype, { 4761 prepMapApi: function() { 4762 if (apiStatus === 'loaded') { 4763 this.createMap(); 4764 } else { 4765 mapsToLoad.push(this); 4766 4767 if (apiStatus !== 'loading') { 4768 apiStatus = 'loading'; 4769 if (typeof window.google === 'undefined' || typeof window.google.maps === 'undefined' ) { 4770 $.getScript( 4771 'https://maps.googleapis.com/maps/api/js?key=' + this.key 4772 ).then(function() { 4773 apiStatus = 'loaded'; 4774 initAllMaps(); 4775 }); 4776 } 4777 } 4778 } 4779 }, 4780 4781 createMap: function() { 4782 var $map = this.$map; 4783 4784 return geolocate($map) 4785 .then( 4786 function(results) { 4787 var mapOptions = { 4788 zoom: config.zoom, 4789 backgroundColor: 'none', 4790 center: results[0].geometry.location, 4791 draggable: false, 4792 clickableIcons: false, 4793 scrollwheel: false, 4794 disableDoubleClickZoom: true, 4795 disableDefaultUI: true 4796 }; 4797 4798 var map = (this.map = new google.maps.Map($map[0], mapOptions)); 4799 var center = (this.center = map.getCenter()); 4800 4801 var marker = new google.maps.Marker({ 4802 map: map, 4803 position: map.getCenter() 4804 }); 4805 4806 google.maps.event.addDomListener( 4807 window, 4808 'resize', 4809 $.debounce(250, function() { 4810 google.maps.event.trigger(map, 'resize'); 4811 map.setCenter(center); 4812 $map.removeAttr('style'); 4813 }) 4814 ); 4815 }.bind(this) 4816 ) 4817 .fail(function() { 4818 var errorMessage; 4819 4820 switch (status) { 4821 case 'ZERO_RESULTS': 4822 errorMessage = errors.addressNoResults; 4823 break; 4824 case 'OVER_QUERY_LIMIT': 4825 errorMessage = errors.addressQueryLimit; 4826 break; 4827 case 'REQUEST_DENIED': 4828 errorMessage = errors.authError; 4829 break; 4830 default: 4831 errorMessage = errors.addressError; 4832 break; 4833 } 4834 4835 // Show errors only to merchant in the editor. 4836 if (Shopify.designMode) { 4837 $map 4838 .parent()
4839 .addClass(classes.mapError) 4840 .html( 4841 '<div class="' + 4842 classes.errorMsg + 4843 '">' + 4844 errorMessage + 4845 '</div>' 4846 ); 4847 } 4848 }); 4849 }, 4850 4851 checkVisibility: function() { 4852 if (theme.isElementVisible(this.$container, 600)) { 4853 this.prepMapApi(); 4854 $(window).off(this.namespace); 4855 } 4856 }, 4857 4858 onUnload: function() { 4859 if (this.$map.length === 0) { 4860 return; 4861 } 4862 // Causes a harmless JS error when a section without an active map is reloaded 4863 google.maps.event.clearListeners(this.map, 'resize'); 4864 } 4865 }); 4866 4867 return Map; 4868 })(); 4869 4870 theme.Blog = (function() { 4871 4872 function Blog(container) { 4873 this.tagFilters(); 4874 } 4875 4876 Blog.prototype = $.extend({}, Blog.prototype, { 4877 tagFilters: function() { 4878 var $filterBy = $('#BlogTagFilter'); 4879 4880 if (!$filterBy.length) { 4881 return; 4882 } 4883 4884 $filterBy.on('change', function() { 4885 location.href = $(this).val(); 4886 }); 4887 }, 4888 4889 onUnload: function() { 4890 4891 } 4892 }); 4893 4894 return Blog; 4895 })(); 4896 4897 theme.Photoswipe = (function() { 4898 var selectors = { 4899 trigger: '.js-photoswipe__zoom', 4900 images: '.photoswipe__image', 4901 activeImage: '.slick-active .photoswipe__image' 4902 }; 4903 4904 function Photoswipe(container, sectionId) { 4905 this.$container = $(container); 4906 this.sectionId = sectionId; 4907 this.namespace = '.photoswipe-' + this.sectionId; 4908 this.gallery; 4909 this.$images; 4910 this.inSlideshow = false; 4911 4912 if (this.$container.attr('data-zoom') === 'false') { 4913 return; 4914 } 4915 4916 if (this.$container.attr('data-has-slideshow') === 'true') { 4917 this.inSlideshow = true; 4918 } 4919 4920 this.init(); 4921 } 4922 4923 Photoswipe.prototype = $.extend({}, Photoswipe.prototype, { 4924 init: function() { 4925 var $trigger = this.$container.find(selectors.trigger); 4926 this.$images = this.$container.find(selectors.images); 4927 var items = this.getImageData(); 4928 4929 // Init gallery on active image 4930 $trigger.on('click' + this.namespace, function(evt) { 4931 if (this.inSlideshow) { 4932 var index = this.$container.find(selectors.activeImage).data('index'); 4933 } else { 4934 var index = $(evt.currentTarget).data('index'); 4935 } 4936 this.initGallery(items, index); 4937 }.bind(this)); 4938 }, 4939 4940 getImageData: function() { 4941 var haveImages = false; 4942 var items = []; 4943 var options = {}; 4944 4945 this.$images.each(function() { 4946 var haveImages = true; 4947 var smallSrc = $(this).prop('currentSrc') || $(this).prop('src'); 4948 var item = { 4949 msrc: smallSrc, 4950 src: $(this).data('photoswipe-src'), 4951 w: $(this).data('photoswipe-width'), 4952 h: $(this).data('photoswipe-height'), 4953 el: $(this)[0], 4954 initialZoomLevel: 0.85 4955 }; 4956 4957 items.push(item); 4958 }); 4959 4960 return items; 4961 }, 4962 4963 initGallery: function(items, index) { 4964 var pswpElement = document.querySelectorAll('.pswp')[0]; 4965 4966 var options = { 4967 allowPanToNext: false, 4968 captionEl: false, 4969 closeOnScroll: false, 4970 counterEl: false, 4971 history: false, 4972 index: index - 1, 4973 pinchToClose: false, 4974 preloaderEl: false, 4975 scaleMode: 'zoom', 4976 shareEl: false, 4977 tapToToggleControls: false, 4978 getThumbBoundsFn: function(index) { 4979 var pageYScroll = window.pageYOffset || document.documentElement.scrollTop; 4980 var thumbnail = items[index].el; 4981 var rect = thumbnail.getBoundingClientRect(); 4982 return {x:rect.left, y:rect.top + pageYScroll, w:rect.width}; 4983 } 4984 } 4985 4986 this.gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options); 4987 4988 this.gallery.init(); 4989 this.gallery.listen('afterChange', this.afterChange.bind(this)); 4990 }, 4991 4992 afterChange: function() { 4993 if (this.inSlideshow) { 4994 var $slideshow = $('#ProductPhotos-' + this.sectionId); 4995 if ($slideshow.hasClass('slick-initialized')) { 4996 var newIndex = this.gallery.getCurrentIndex(); 4997 $slideshow.slick('slickGoTo', newIndex); 4998 } 4999 } 5000 } 5001 }); 5002 5003 return Photoswipe; 5004 })(); 5005 5006 5007 5008 // Medium breakpoint is also set in theme.scss.liquid and inline throughout some templates.
5009 // Do not change values unless you know what you're doing. 5010 theme.bp = {}; 5011 theme.bp.smallUp = 590; 5012 theme.bp.small = theme.bp.smallUp - 1; 5013 5014 theme.config = { 5015 bpSmall: false, 5016 hasSessionStorage: true, 5017 mediaQuerySmall: 'screen and (max-width: '+ theme.bp.small +'px)', 5018 mediaQuerySmallUp: 'screen and (min-width: '+ theme.bp.smallUp +'px)', 5019 youTubeReady: false, 5020 vimeoReady: false, 5021 vimeoLoading: false, 5022 isTouch: ('ontouchstart' in window) || window.DocumentTouch && window.document instanceof DocumentTouch || window.navigator.maxTouchPoints || window.navigator.msMaxTouchPoints ? true : false 5023 }; 5024 5025 window.onYouTubeIframeAPIReady = function() { 5026 theme.config.youTubeReady = true; 5027 $('body').trigger('youTubeReady'); 5028 } 5029 5030 window.loadYouTube = function() { 5031 if (theme.config.youtubeReady) { 5032 return; 5033 } 5034 5035 var tag = document.createElement('script'); 5036 tag.src = "https://www.youtube.com/iframe_api"; 5037 var firstScriptTag = document.getElementsByTagName('script')[0]; 5038 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 5039 } 5040 5041 window.loadVimeo = function() { 5042 if (theme.config.vimeoLoading) { 5043 return; 5044 } 5045 5046 if (!theme.config.vimeoReady) { 5047 theme.config.vimeoLoading = true; 5048 var tag = document.createElement('script'); 5049 tag.src = "https://player.vimeo.com/api/player.js"; 5050 var firstScriptTag = document.getElementsByTagName('script')[0]; 5051 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 5052 5053 // Because there's no way to check for the Vimeo API being loaded 5054 // asynchronously, we use this terrible timeout to wait for it being ready 5055 checkIfVimeoIsReady() 5056 .then(function() { 5057 theme.config.vimeoReady = true; 5058 theme.config.vimeoLoading = false; 5059 $('body').trigger('vimeoReady'); 5060 }) 5061 .fail(function() { 5062 // No vimeo API to talk to 5063 }); 5064 } 5065 } 5066 5067 function checkIfVimeoIsReady() { 5068 var deferred = $.Deferred(); 5069 var wait; 5070 var timeout; 5071 5072 wait = setInterval(function() { 5073 if (!Vimeo) { 5074 return; 5075 } 5076 5077 clearInterval(wait); 5078 clearTimeout(timeout); 5079 deferred.resolve(); 5080 }, 500); 5081 5082 timeout = setTimeout(function() { 5083 clearInterval(wait); 5084 deferred.reject(); 5085 }, 4000); // subjective. test up to 8 times over 4 seconds 5086 5087 return deferred; 5088 } 5089 5090 theme.init = function() { 5091 theme.setGlobals(); 5092 theme.pageTransitions(); 5093 theme.initQuickShop(); 5094 theme.videoModal(); 5095 theme.articleImages.init(); 5096 theme.customerTemplates.init(); 5097 theme.collapsibles.init(); 5098 5099 slate.rte.init(); 5100 5101 AOS.init({ 5102 easing: 'ease-out-quad', 5103 once: true, 5104 offset: 60, 5105 disableMutationObserver: true 5106 }); 5107 5108 $(document.documentElement).on('keyup.tab', function(evt) { 5109 if (evt.keyCode === 9) { 5110 $(document.documentElement).addClass('tab-outline'); 5111 $(document.documentElement).off('keyup.tab'); 5112 } 5113 }); 5114 }; 5115 5116 theme.setGlobals = function() { 5117 theme.config.hasSessionStorage = theme.isSessionStorageSupported(); 5118 5119 if (theme.config.isTouch) { 5120 $('body').addClass('supports-touch'); 5121 } 5122 5123 enquire.register(theme.config.mediaQuerySmall, { 5124 match: function() { 5125 theme.config.bpSmall = true; 5126 $('body').trigger('matchSmall'); 5127 }, 5128 unmatch: function() { 5129 theme.config.bpSmall = false; 5130 $('body').trigger('unmatchSmall'); 5131 } 5132 }); 5133 }; 5134 5135 theme.loadImageSection = function($container) { 5136 // Wait until images inside container have lazyloaded class 5137 function setAsLoaded() { 5138 $container.removeClass('loading loading--delayed').addClass('loaded'); 5139 } 5140 5141 function checkForLazyloadedImage() { 5142 return $container.find('.lazyloaded').length; 5143 } 5144 5145 // If it has SVGs it's in the onboarding state so set as loaded 5146 if ($container.find('svg').length) { 5147 setAsLoaded(); 5148 return; 5149 }; 5150 5151 if (checkForLazyloadedImage() > 0) { 5152 setAsLoaded(); 5153 return; 5154 } 5155 5156 var interval = setInterval(function() { 5157 if (checkForLazyloadedImage() > 0) { 5158 clearInterval(interval); 5159 setAsLoaded(); 5160 } 5161 }, 80); 5162 } 5163 5164 theme.isSessionStorageSupported = function() { 5165 // Return false if we are in an iframe without access to sessionStorage 5166 if (window.self !== window.top) { 5167 return false;
5168 } 5169 5170 var testKey = 'test'; 5171 var storage = window.sessionStorage; 5172 try { 5173 storage.setItem(testKey, '1'); 5174 storage.removeItem(testKey); 5175 return true; 5176 } catch (error) { 5177 return false; 5178 } 5179 }; 5180 5181 theme.isElementVisible = function($el, threshold) { 5182 var rect = $el[0].getBoundingClientRect(); 5183 var windowHeight = window.innerHeight || document.documentElement.clientHeight; 5184 threshold = threshold ? threshold : 0; 5185 5186 return ( 5187 rect.bottom >= 0 && 5188 rect.right >= 0 && 5189 rect.top <= (windowHeight + threshold) && 5190 rect.left <= (window.innerWidth || document.documentElement.clientWidth) 5191 ); 5192 }; 5193 5194 theme.pageTransitions = function() { 5195 if ($('body').data('transitions') == true) { 5196 5197 // Hack test to fix Safari page cache issue. 5198 // window.onpageshow doesn't always run when navigating 5199 // back to the page, so the unloading class remains, leaving 5200 // a white page. Setting a timeout to remove that class when leaving 5201 // the page actually finishes running when they come back. 5202 if (!!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/)) { 5203 $('a').on('click', function() { 5204 window.setTimeout(function() { 5205 $('body').removeClass('unloading'); 5206 }, 1200); 5207 }); 5208 } 5209 5210 // Add disable transition class to malito, anchor, and YouTube links 5211 $('a[href^="mailto:"], a[href^="#"], a[target="_blank"], a[href*="youtube.com/watch"], a[href*="youtu.be/"]').each(function() { 5212 $(this).addClass('js-no-transition'); 5213 }); 5214 5215 $('a:not(.js-no-transition)').bind('click', function(evt) { 5216 if (evt.metaKey) return true; 5217 evt.preventDefault(); 5218 $('body').addClass('unloading'); 5219 var src = $(this).attr('href'); 5220 window.setTimeout(function() { 5221 location.href = src; 5222 }, 50); 5223 }); 5224 5225 $('a.mobile-nav__link').bind('click', function() { 5226 theme.NavDrawer.close(); 5227 }); 5228 } 5229 }; 5230 5231 theme.reinitProductGridItem = function($scope) { 5232 if (AOS) { 5233 AOS.refreshHard(); 5234 } 5235 5236 // Refresh currency 5237 if (theme.settings.currenciesEnabled) { 5238 theme.currencySwitcher.ajaxrefresh(); 5239 } 5240 5241 // Reload quick shop buttons 5242 theme.initQuickShop(true); 5243 5244 // Refresh reviews app 5245 if (window.SPR) { 5246 SPR.initDomEls();SPR.loadBadges(); 5247 } 5248 5249 // Re-register product templates in quick view modals. 5250 // Will not double-register. 5251 sections.register('product-template', theme.Product, $scope); 5252 5253 // Re-hook up collapsible box triggers 5254 theme.collapsibles.init(); 5255 }; 5256 5257 window.onpageshow = function(evt) { 5258 // Removes unload class when returning to page via history 5259 if (evt.persisted) { 5260 $('body').removeClass('unloading'); 5261 } 5262 }; 5263 5264 $(document).ready(function() { 5265 theme.init(); 5266 5267 window.sections = new theme.Sections(); 5268 sections.register('header-section', theme.HeaderSection); 5269 sections.register('slideshow-section', theme.SlideshowSection); 5270 sections.register('video-section', theme.VideoSection); 5271 sections.register('product', theme.Product); 5272 sections.register('product-recommendations', theme.Recommendations); 5273 sections.register('product-template', theme.Product); 5274 sections.register('collection-template', theme.Collection); 5275 sections.register('featured-content-section', theme.FeaturedContentSection); 5276 sections.register('collection-switcher', theme.CollectionSwitcher); 5277 sections.register('testimonials', theme.Testimonials); 5278 sections.register('instagram', theme.Instagram); 5279 sections.register('newsletter-popup', theme.NewsletterPopup); 5280 sections.register('fading-images', theme.FadingImages); 5281 sections.register('map', theme.Maps); 5282 sections.register('blog', theme.Blog); 5283 sections.register('photoswipe', theme.Photoswipe); 5284 }); 5285})(theme.jQuery);
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.