1window.theme = window.theme || {}; 2 3/* ================ SLATE ================ */ 4window.theme = window.theme || {}; 5 6theme.Sections = function Sections() { 7 this.constructors = {}; 8 this.instances = []; 9 10 $(document) 11 .on('shopify:section:load', this._onSectionLoad.bind(this)) 12 .on('shopify:section:unload', this._onSectionUnload.bind(this)) 13 .on('shopify:section:select', this._onSelect.bind(this)) 14 .on('shopify:section:deselect', this._onDeselect.bind(this)) 15 .on('shopify:block:select', this._onBlockSelect.bind(this)) 16 .on('shopify:block:deselect', this._onBlockDeselect.bind(this)); 17}; 18 19theme.Sections.prototype = _.assignIn({}, theme.Sections.prototype, { 20 _createInstance: function(container, constructor) { 21 var $container = $(container); 22 var id = $container.attr('data-section-id'); 23 var type = $container.attr('data-section-type'); 24 25 constructor = constructor || this.constructors[type]; 26 27 if (_.isUndefined(constructor)) { 28 return; 29 } 30 31 var instance = _.assignIn(new constructor(container), { 32 id: id, 33 type: type, 34 container: container 35 }); 36 37 this.instances.push(instance); 38 }, 39 40 _onSectionLoad: function(evt) { 41 var container = $('[data-section-id]', evt.target)[0]; 42 if (container) { 43 this._createInstance(container); 44 } 45 }, 46 47 _onSectionUnload: function(evt) { 48 this.instances = _.filter(this.instances, function(instance) { 49 var isEventInstance = instance.id === evt.detail.sectionId; 50 51 if (isEventInstance) { 52 if (_.isFunction(instance.onUnload)) { 53 instance.onUnload(evt); 54 } 55 } 56 57 return !isEventInstance; 58 }); 59 }, 60 61 _onSelect: function(evt) { 62 // eslint-disable-next-line no-shadow 63 var instance = _.find(this.instances, function(instance) { 64 return instance.id === evt.detail.sectionId; 65 }); 66 67 if (!_.isUndefined(instance) && _.isFunction(instance.onSelect)) { 68 instance.onSelect(evt); 69 } 70 }, 71 72 _onDeselect: function(evt) { 73 // eslint-disable-next-line no-shadow 74 var instance = _.find(this.instances, function(instance) { 75 return instance.id === evt.detail.sectionId; 76 }); 77 78 if (!_.isUndefined(instance) && _.isFunction(instance.onDeselect)) { 79 instance.onDeselect(evt); 80 } 81 }, 82 83 _onBlockSelect: function(evt) { 84 // eslint-disable-next-line no-shadow 85 var instance = _.find(this.instances, function(instance) { 86 return instance.id === evt.detail.sectionId; 87 }); 88 89 if (!_.isUndefined(instance) && _.isFunction(instance.onBlockSelect)) { 90 instance.onBlockSelect(evt); 91 } 92 }, 93 94 _onBlockDeselect: function(evt) { 95 // eslint-disable-next-line no-shadow 96 var instance = _.find(this.instances, function(instance) { 97 return instance.id === evt.detail.sectionId; 98 }); 99 100 if (!_.isUndefined(instance) && _.isFunction(instance.onBlockDeselect)) { 101 instance.onBlockDeselect(evt); 102 } 103 }, 104 105 register: function(type, constructor) { 106 this.constructors[type] = constructor; 107 108 $('[data-section-type=' + type + ']').each( 109 function(index, container) { 110 this._createInstance(container, constructor); 111 }.bind(this) 112 ); 113 } 114}); 115 116window.slate = window.slate || {}; 117 118/** 119 * Slate utilities 120 * ----------------------------------------------------------------------------- 121 * A collection of useful utilities to help build your theme 122 * 123 * 124 * @namespace utils 125 */ 126 127slate.utils = { 128 /** 129 * Get the query params in a Url 130 * Ex 131 * https://mysite.com/search?q=noodles&b 132 * getParameterByName('q') = "noodles"
133 * getParameterByName('b') = "" (empty value) 134 * getParameterByName('test') = null (absent) 135 */ 136 getParameterByName: function(name, url) { 137 if (!url) url = window.location.href; 138 name = name.replace(/[[\]]/g, '\\$&'); 139 var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), 140 results = regex.exec(url); 141 if (!results) return null; 142 if (!results[2]) return ''; 143 return decodeURIComponent(results[2].replace(/\+/g, ' ')); 144 }, 145 146 resizeSelects: function($selects) { 147 $selects.each(function() { 148 var $this = $(this); 149 var arrowWidth = 10; 150 // create test element 151 var text = $this.find('option:selected').text(); 152 var $test = $('<span>').html(text); 153 154 // add to body, get width, and get out 155 $test.appendTo('body'); 156 var width = $test.width(); 157 $test.remove(); 158 159 // set select width 160 $this.width(width + arrowWidth); 161 }); 162 }, 163 164 keyboardKeys: { 165 TAB: 9, 166 ENTER: 13, 167 ESCAPE: 27, 168 LEFTARROW: 37, 169 RIGHTARROW: 39 170 } 171}; 172 173window.slate = window.slate || {}; 174 175/** 176 * iFrames 177 * ----------------------------------------------------------------------------- 178 * Wrap videos in div to force responsive layout. 179 * 180 * @namespace iframes 181 */ 182 183slate.rte = { 184 /** 185 * Wrap tables in a container div to make them scrollable when needed 186 * 187 * @param {object} options - Options to be used 188 * @param {jquery} options.$tables - jquery object(s) of the table(s) to wrap 189 * @param {string} options.tableWrapperClass - table wrapper class name 190 */ 191 wrapTable: function(options) { 192 options.$tables.wrap( 193 '<div class="' + options.tableWrapperClass + '"></div>' 194 ); 195 }, 196 197 /** 198 * Wrap iframes in a container div to make them responsive 199 * 200 * @param {object} options - Options to be used 201 * @param {jquery} options.$iframes - jquery object(s) of the iframe(s) to wrap 202 * @param {string} options.iframeWrapperClass - class name used on the wrapping div 203 */ 204 wrapIframe: function(options) { 205 options.$iframes.each(function() { 206 // Add wrapper to make video responsive 207 $(this).wrap('<div class="' + options.iframeWrapperClass + '"></div>'); 208 209 // Re-set the src attribute on each iframe after page load 210 // for Chrome's "incorrect iFrame content on 'back'" bug. 211 // https://code.google.com/p/chromium/issues/detail?id=395791 212 // Need to specifically target video and admin bar 213 this.src = this.src; 214 }); 215 } 216}; 217 218window.slate = window.slate || {}; 219 220/** 221 * A11y Helpers 222 * ----------------------------------------------------------------------------- 223 * A collection of useful functions that help make your theme more accessible 224 * to users with visual impairments. 225 * 226 * 227 * @namespace a11y 228 */ 229 230slate.a11y = { 231 /** 232 * For use when focus shifts to a container rather than a link 233 * eg for In-page links, after scroll, focus shifts to content area so that 234 * next `tab` is where user expects if focusing a link, just $link.focus(); 235 * 236 * @param {JQuery} $element - The element to be acted upon 237 */ 238 pageLinkFocus: function($element) { 239 var focusClass = 'js-focus-hidden'; 240 241 $element 242 .first() 243 .attr('tabIndex', '-1') 244 .focus() 245 .addClass(focusClass) 246 .one('blur', callback); 247 248 function callback() { 249 $element 250 .first() 251 .removeClass(focusClass) 252 .removeAttr('tabindex'); 253 } 254 }, 255 256 /** 257 * If there's a hash in the url, focus the appropriate element 258 */ 259 focusHash: function() { 260 var hash = window.location.hash; 261 262 // is there a hash in the url? is it an element on the page? 263 if (hash && document.getElementById(hash.slice(1))) { 264 this.pageLinkFocus($(hash)); 265 } 266 }, 267 268 /** 269 * When an in-page (url w/hash) link is clicked, focus the appropriate element 270 */ 271 bindInPageLinks: function() { 272 $('a[href*=#]').on( 273 'click', 274 function(evt) { 275 this.pageLinkFocus($(evt.currentTarget.hash)); 276 }.bind(this) 277 ); 278 }, 279 280 /** 281 * Traps the focus in a particular container 282 * 283 * @param {object} options - Options to be used 284 * @param {jQuery} options.$container - Container to trap focus within 285 * @param {jQuery}
285 options.$elementToFocus - Element to be focused when focus leaves container 286 * @param {string} options.namespace - Namespace used for new focus event handler 287 */ 288 trapFocus: function(options) { 289 var eventsName = { 290 focusin: options.namespace ? 'focusin.' + options.namespace : 'focusin', 291 focusout: options.namespace 292 ? 'focusout.' + options.namespace 293 : 'focusout', 294 keydown: options.namespace 295 ? 'keydown.' + options.namespace 296 : 'keydown.handleFocus' 297 }; 298 299 /** 300 * Get every possible visible focusable element 301 */ 302 var $focusableElements = options.$container.find( 303 $( 304 'button, [href], input, select, textarea, [tabindex]:not([tabindex^="-"])' 305 ).filter(':visible') 306 ); 307 var firstFocusable = $focusableElements[0]; 308 var lastFocusable = $focusableElements[$focusableElements.length - 1]; 309 310 if (!options.$elementToFocus) { 311 options.$elementToFocus = options.$container; 312 } 313 314 function _manageFocus(evt) { 315 if (evt.keyCode !== slate.utils.keyboardKeys.TAB) return; 316 317 /** 318 * On the last focusable element and tab forward, 319 * focus the first element. 320 */ 321 if (evt.target === lastFocusable && !evt.shiftKey) { 322 evt.preventDefault(); 323 firstFocusable.focus(); 324 } 325 /** 326 * On the first focusable element and tab backward, 327 * focus the last element. 328 */ 329 if (evt.target === firstFocusable && evt.shiftKey) { 330 evt.preventDefault(); 331 lastFocusable.focus(); 332 } 333 } 334 335 options.$container.attr('tabindex', '-1'); 336 options.$elementToFocus.focus(); 337 338 $(document).off('focusin'); 339 340 $(document).on(eventsName.focusout, function() { 341 $(document).off(eventsName.keydown); 342 }); 343 344 $(document).on(eventsName.focusin, function(evt) { 345 if (evt.target !== lastFocusable && evt.target !== firstFocusable) return; 346 347 $(document).on(eventsName.keydown, function(evt) { 348 _manageFocus(evt); 349 }); 350 }); 351 }, 352 353 /** 354 * Removes the trap of focus in a particular container 355 * 356 * @param {object} options - Options to be used 357 * @param {jQuery} options.$container - Container to trap focus within 358 * @param {string} options.namespace - Namespace used for new focus event handler 359 */ 360 removeTrapFocus: function(options) { 361 var eventName = options.namespace 362 ? 'focusin.' + options.namespace 363 : 'focusin'; 364 365 if (options.$container && options.$container.length) { 366 options.$container.removeAttr('tabindex'); 367 } 368 369 $(document).off(eventName); 370 }, 371 372 /** 373 * Add aria-describedby attribute to external and new window links 374 * 375 * @param {object} options - Options to be used 376 * @param {object} options.messages - Custom messages to be used 377 * @param {jQuery} options.$links - Specific links to be targeted 378 */ 379 accessibleLinks: function(options) { 380 var body = document.querySelector('body'); 381 382 var idSelectors = { 383 newWindow: 'a11y-new-window-message', 384 external: 'a11y-external-message', 385 newWindowExternal: 'a11y-new-window-external-message' 386 }; 387 388 if (options.$links === undefined || !options.$links.jquery) { 389 options.$links = $('a[href]:not([aria-describedby])'); 390 } 391 392 function generateHTML(customMessages) { 393 if (typeof customMessages !== 'object') { 394 customMessages = {}; 395 } 396 397 var messages = $.extend( 398 { 399 newWindow: 'Opens in a new window.', 400 external: 'Opens external website.', 401 newWindowExternal: 'Opens external website in a new window.' 402 }, 403 customMessages 404 ); 405 406 var container = document.createElement('ul'); 407 var htmlMessages = ''; 408 409 for (var message in messages) { 410 htmlMessages += 411 '<li id=' + idSelectors[message] + '>' + messages[message] + '</li>'; 412 } 413 414 container.setAttribute('hidden', true); 415 container.innerHTML = htmlMessages; 416 417 body.appendChild(container); 418 } 419 420 function _externalSite($link) { 421 var hostname = window.location.hostname; 422 423 return $link[0].hostname !== hostname; 424 } 425 426 $.each(options.$links, function() { 427 var $link = $(this); 428 var target = $link.attr('target'); 429 var rel = $link.attr('rel'); 430 var isExternal = _externalSite($link); 431 var isTargetBlank = target === '_blank'; 432 433 if (isExternal) { 434 $link.attr('aria-describedby', idSelectors.external); 435 } 436 if (isTargetBlank) { 437 if (rel === undefined || rel.indexOf('noopener') === -1) { 438 $link.attr('rel', function(i, val) { 439 var relValue = val === undefined ? '' : val + ' '; 440 return relValue + 'noopener'; 441 }); 442 } 443 $link.attr('aria-describedby', idSelectors.newWindow); 444 } 445 if (isExternal && isTargetBlank) { 446 $link.attr('aria-describedby', idSelectors.newWindowExternal); 447 } 448 }); 449 450 generateHTML(options.messages); 451 } 452}; 453 454/** 455 * Image Helper Functions 456 * ----------------------------------------------------------------------------- 457 * A collection of functions that help with basic image operations. 458 * 459 */ 460 461theme.Images = (function() { 462 /** 463 * Preloads an image in memory and uses the browsers cache to store it until needed. 464 * 465 * @param {Array} images - A list of image urls 466 * @param {String} size - A shopify image size attribute 467 */ 468 469 function preload(images, size) { 470 if (typeof images === 'string') { 471 images = [images]; 472 } 473 474 for (var i = 0; i < images.length; i++) { 475 var image = images[i]; 476 this.loadImage(this.getSizedImageUrl(image, size)); 477 } 478 } 479 480 /** 481 * Loads and caches an image in the browsers cache. 482 * @param {string} path - An image url 483 */ 484 function loadImage(path) { 485 new Image().src = path; 486 } 487 488 /** 489 * Swaps the src of an image for another OR returns the imageURL to the callback function 490 * @param image 491 * @param element 492 * @param callback 493 */ 494 function switchImage(image, element, callback) { 495 var size = this.imageSize(element.src);
496 var imageUrl = this.getSizedImageUrl(image.src, size); 497 498 if (callback) { 499 callback(imageUrl, image, element); // eslint-disable-line callback-return 500 } else { 501 element.src = imageUrl; 502 } 503 } 504 505 /** 506 * +++ Useful 507 * Find the Shopify image attribute size 508 * 509 * @param {string} src 510 * @returns {null} 511 */ 512 function imageSize(src) { 513 var match = src.match( 514 /.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\\.@]/ 515 ); 516 517 if (match !== null) { 518 if (match[2] !== undefined) { 519 return match[1] + match[2]; 520 } else { 521 return match[1]; 522 } 523 } else { 524 return null; 525 } 526 } 527 528 /** 529 * +++ Useful 530 * Adds a Shopify size attribute to a URL 531 * 532 * @param src 533 * @param size 534 * @returns {*} 535 */ 536 function getSizedImageUrl(src, size) { 537 if (size === null) { 538 return src; 539 } 540 541 if (size === 'master') { 542 return this.removeProtocol(src); 543 } 544 545 var match = src.match( 546 /\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i 547 ); 548 549 if (match !== null) { 550 var prefix = src.split(match[0]); 551 var suffix = match[0]; 552 553 return this.removeProtocol(prefix[0] + '_' + size + suffix); 554 } 555 556 return null; 557 } 558 559 function removeProtocol(path) { 560 return path.replace(/http(s)?:/, ''); 561 } 562 563 return { 564 preload: preload, 565 loadImage: loadImage, 566 switchImage: switchImage, 567 imageSize: imageSize, 568 getSizedImageUrl: getSizedImageUrl, 569 removeProtocol: removeProtocol 570 }; 571})(); 572 573/** 574 * Currency Helpers 575 * ----------------------------------------------------------------------------- 576 * A collection of useful functions that help with currency formatting 577 * 578 * Current contents 579 * - formatMoney - Takes an amount in cents and returns it as a formatted dollar value. 580 * 581 * Alternatives 582 * - Accounting.js - http://openexchangerates.github.io/accounting.js/ 583 * 584 */ 585 586theme.Currency = (function() { 587 var moneyFormat = '${{amount}}'; // eslint-disable-line camelcase 588 589 function formatMoney(cents, format) { 590 if (typeof cents === 'string') { 591 cents = cents.replace('.', ''); 592 } 593 var value = ''; 594 var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/; 595 var formatString = format || moneyFormat; 596 597 function formatWithDelimiters(number, precision, thousands, decimal) { 598 thousands = thousands || ','; 599 decimal = decimal || '.'; 600 601 if (isNaN(number) || number === null) { 602 return 0; 603 } 604 605 number = (number / 100.0).toFixed(precision); 606 607 var parts = number.split('.'); 608 var dollarsAmount = parts[0].replace( 609 /(\d)(?=(\d\d\d)+(?!\d))/g, 610 '$1' + thousands 611 ); 612 var centsAmount = parts[1] ? decimal + parts[1] : ''; 613 614 return dollarsAmount + centsAmount; 615 } 616 617 switch (formatString.match(placeholderRegex)[1]) { 618 case 'amount': 619 value = formatWithDelimiters(cents, 2); 620 break; 621 case 'amount_no_decimals': 622 value = formatWithDelimiters(cents, 0); 623 break; 624 case 'amount_with_comma_separator': 625 value = formatWithDelimiters(cents, 2, '.', ','); 626 break; 627 case 'amount_no_decimals_with_comma_separator': 628 value = formatWithDelimiters(cents, 0, '.', ','); 629 break; 630 case 'amount_no_decimals_with_space_separator': 631 value = formatWithDelimiters(cents, 0, ' '); 632 break; 633 case 'amount_with_apostrophe_separator': 634 value = formatWithDelimiters(cents, 2, "'"); 635 break; 636 } 637 638 return formatString.replace(placeholderRegex, value); 639 } 640 641 return { 642 formatMoney: formatMoney 643 }; 644})(); 645 646/** 647 * Variant Selection scripts 648 * ------------------------------------------------------------------------------ 649 * 650 * Handles change events from the variant inputs in any `cart/add` forms that may 651 * exist. Also updates the master select and triggers updates when the variants 652 * price or image changes. 653 * 654 * @namespace variants 655 */ 656 657slate.Variants = (function() { 658 /** 659 * Variant constructor 660 * 661 * @param {object} options - Settings from `product.js` 662 */ 663 function Variants(options) { 664 this.$container = options.$container; 665 this.product = options.product; 666 this.singleOptionSelector = options.singleOptionSelector; 667 this.originalSelectorId = options.originalSelectorId; 668 this.enableHistoryState = options.enableHistoryState; 669 this.currentVariant = this._getVariantFromOptions(); 670 671 $(this.singleOptionSelector, this.$container).on( 672 'change', 673 this._onSelectChange.bind(this) 674 ); 675 } 676 677 Variants.prototype = _.assignIn({}, Variants.prototype, { 678 /** 679 * Get the currently selected options from add-to-cart form. Works with all 680 * form input elements. 681 * 682 * @return {array} options - Values of currently selected variants 683 */ 684 _getCurrentOptions: function() { 685 var currentOptions = _.map( 686 $(this.singleOptionSelector, this.$container), 687 function(element) { 688 var $element = $(element); 689 var type = $element.attr('type'); 690 var currentOption = {}; 691 692 if (type === 'radio' || type === 'checkbox') { 693 if ($element[0].checked) { 694 currentOption.value = $element.val(); 695 currentOption.index = $element.data('index'); 696 697 return currentOption; 698 } else { 699 return false;
700 } 701 } else { 702 currentOption.value = $element.val(); 703 currentOption.index = $element.data('index'); 704 705 return currentOption; 706 } 707 } 708 ); 709 710 // remove any unchecked input values if using radio buttons or checkboxes 711 currentOptions = _.compact(currentOptions); 712 713 return currentOptions; 714 }, 715 716 /** 717 * Find variant based on selected values. 718 * 719 * @param {array} selectedValues - Values of variant inputs 720 * @return {object || undefined} found - Variant object from product.variants 721 */ 722 _getVariantFromOptions: function() { 723 var selectedValues = this._getCurrentOptions(); 724 var variants = this.product.variants; 725 726 var found = _.find(variants, function(variant) { 727 return selectedValues.every(function(values) { 728 return _.isEqual(variant[values.index], values.value); 729 }); 730 }); 731 732 return found; 733 }, 734 735 /** 736 * Event handler for when a variant input changes. 737 */ 738 _onSelectChange: function() { 739 var variant = this._getVariantFromOptions(); 740 741 this.$container.trigger({ 742 type: 'variantChange', 743 variant: variant 744 }); 745 746 if (!variant) { 747 return; 748 } 749 750 this._updateMasterSelect(variant); 751 this._updateImages(variant); 752 this._updatePrice(variant); 753 this._updateSKU(variant); 754 this.currentVariant = variant; 755 756 if (this.enableHistoryState) { 757 this._updateHistoryState(variant); 758 } 759 }, 760 761 /** 762 * Trigger event when variant image changes 763 * 764 * @param {object} variant - Currently selected variant 765 * @return {event} variantImageChange 766 */ 767 _updateImages: function(variant) { 768 var variantImage = variant.featured_image || {}; 769 var currentVariantImage = this.currentVariant.featured_image || {}; 770 771 if ( 772 !variant.featured_image || 773 variantImage.src === currentVariantImage.src 774 ) { 775 return; 776 } 777 778 this.$container.trigger({ 779 type: 'variantImageChange', 780 variant: variant 781 }); 782 }, 783 784 /** 785 * Trigger event when variant price changes. 786 * 787 * @param {object} variant - Currently selected variant 788 * @return {event} variantPriceChange 789 */ 790 _updatePrice: function(variant) { 791 if ( 792 variant.price === this.currentVariant.price && 793 variant.compare_at_price === this.currentVariant.compare_at_price 794 ) { 795 return; 796 } 797 798 this.$container.trigger({ 799 type: 'variantPriceChange', 800 variant: variant 801 }); 802 }, 803 804 /** 805 * Trigger event when variant sku changes. 806 * 807 * @param {object} variant - Currently selected variant 808 * @return {event} variantSKUChange 809 */ 810 _updateSKU: function(variant) { 811 if (variant.sku === this.currentVariant.sku) { 812 return; 813 } 814 815 this.$container.trigger({ 816 type: 'variantSKUChange', 817 variant: variant 818 }); 819 }, 820 821 /** 822 * Update history state for product deeplinking 823 * 824 * @param {variant} variant - Currently selected variant 825 * @return {k} [description] 826 */ 827 _updateHistoryState: function(variant) { 828 if (!history.replaceState || !variant) { 829 return; 830 } 831 832 var newurl = 833 window.location.protocol + 834 '//' + 835 window.location.host + 836 window.location.pathname + 837 '?variant=' + 838 variant.id; 839 window.history.propertyIsEnumerable({ path: newurl }, '', newurl); 840 }, 841 842 /** 843 * Update hidden master select of variant change 844 * 845 * @param {variant} variant - Currently selected variant 846 */ 847 _updateMasterSelect: function(variant) { 848 $(this.originalSelectorId, this.$container).val(variant.id); 849 } 850 }); 851 852 return Variants; 853})(); 854 855this.Shopify = this.Shopify || {}; 856this.Shopify.theme = this.Shopify.theme || {}; 857this.Shopify.theme.PredictiveSearch = (function() { 858 'use strict'; 859 860 function validateQuery(query) { 861 var error; 862 863 if (query === null || query === undefined) { 864 error = new TypeError("'query' is missing"); 865 error.type = 'argument'; 866 throw error; 867 } 868 869 if (typeof query !== 'string') { 870 error = new TypeError("'query' is not a string"); 871 error.type = 'argument'; 872 throw error; 873 } 874 } 875 876 function GenericError() { 877 var error = Error.call(this); 878
879 error.name = 'Server error'; 880 error.message = 'Something went wrong on the server'; 881 error.status = 500; 882 883 return error; 884 } 885 886 function NotFoundError(status) { 887 var error = Error.call(this); 888 889 error.name = 'Not found'; 890 error.message = 'Not found'; 891 error.status = status; 892 893 return error; 894 } 895 896 function ServerError() { 897 var error = Error.call(this); 898 899 error.name = 'Server error'; 900 error.message = 'Something went wrong on the server'; 901 error.status = 500; 902 903 return error; 904 } 905 906 function ContentTypeError(status) { 907 var error = Error.call(this); 908 909 error.name = 'Content-Type error'; 910 error.message = 'Content-Type was not provided or is of wrong type'; 911 error.status = status; 912 913 return error; 914 } 915 916 function JsonParseError(status) { 917 var error = Error.call(this); 918 919 error.name = 'JSON parse error'; 920 error.message = 'JSON syntax error'; 921 error.status = status; 922 923 return error; 924 } 925 926 function ThrottledError(status, name, message, retryAfter) { 927 var error = Error.call(this); 928 929 error.name = name; 930 error.message = message; 931 error.status = status; 932 error.retryAfter = retryAfter; 933 934 return error; 935 } 936 937 function InvalidParameterError(status, name, message) { 938 var error = Error.call(this); 939 940 error.name = name; 941 error.message = message; 942 error.status = status; 943 944 return error; 945 } 946 947 function ExpectationFailedError(status, name, message) { 948 var error = Error.call(this); 949 950 error.name = name; 951 error.message = message; 952 error.status = status; 953 954 return error; 955 } 956 957 function request(configParams, query, onSuccess, onError) { 958 var xhr = new XMLHttpRequest(); 959 960 xhr.onreadystatechange = function() { 961 if (xhr.readyState !== XMLHttpRequest.DONE) { 962 return; 963 } 964 965 var contentType = xhr.getResponseHeader('Content-Type'); 966 967 if (xhr.status >= 500) { 968 onError(new ServerError()); 969 970 return; 971 } 972 973 if (xhr.status === 404) { 974 onError(new NotFoundError(xhr.status)); 975 976 return; 977 } 978 979 if ( 980 typeof contentType !== 'string' || 981 contentType.toLowerCase().match('application/json') === null 982 ) { 983 onError(new ContentTypeError(xhr.status)); 984 985 return; 986 } 987 988 if (xhr.status === 417) { 989 try { 990 var invalidParameterJson = JSON.parse(xhr.responseText); 991 992 onError( 993 new InvalidParameterError( 994 xhr.status, 995 invalidParameterJson.message, 996 invalidParameterJson.description 997 ) 998 ); 999 } catch (error) { 1000 onError(new JsonParseError(xhr.status)); 1001 } 1002 1003 return; 1004 } 1005 1006 if (xhr.status === 422) { 1007 try { 1008 var expectationFailedJson = JSON.parse(xhr.responseText); 1009 1010 onError( 1011 new ExpectationFailedError( 1012 xhr.status, 1013 expectationFailedJson.message, 1014 expectationFailedJson.description 1015 ) 1016 ); 1017 } catch (error) { 1018 onError(new JsonParseError(xhr.status)); 1019 } 1020 1021 return; 1022 } 1023 1024 if (xhr.status === 429) { 1025 try { 1026 var throttledJson = JSON.parse(xhr.responseText); 1027 1028 onError( 1029 new ThrottledError( 1030 xhr.status, 1031 throttledJson.message, 1032 throttledJson.description, 1033 xhr.getResponseHeader('Retry-After') 1034 ) 1035 ); 1036 } catch (error) { 1037 onError(new JsonParseError(xhr.status)); 1038 } 1039 1040 return; 1041 } 1042 1043 if (xhr.status === 200) { 1044 try { 1045 var res = JSON.parse(xhr.responseText); 1046 res.query = query; 1047 onSuccess(res); 1048 } catch (error) { 1049 onError(new JsonParseError(xhr.status)); 1050 } 1051 1052 return; 1053 } 1054 1055 try { 1056 var genericErrorJson = JSON.parse(xhr.responseText); 1057 onError( 1058 new GenericError( 1059 xhr.status, 1060 genericErrorJson.message, 1061 genericErrorJson.description 1062 ) 1063 ); 1064 } catch (error) { 1065 onError(new JsonParseError(xhr.status)); 1066 } 1067 1068 return; 1069 }; 1070 1071 xhr.open( 1072 'get', 1073 '/search/suggest.json?q=' + encodeURIComponent(query) + '&' + configParams 1074 ); 1075 1076 xhr.setRequestHeader('Content-Type', 'application/json'); 1077 1078 xhr.send(); 1079 } 1080 1081 function Cache(config) { 1082 this._store = {}; 1083 this._keys = [];
1084 if (config && config.bucketSize) { 1085 this.bucketSize = config.bucketSize; 1086 } else { 1087 this.bucketSize = 20; 1088 } 1089 } 1090 1091 Cache.prototype.set = function(key, value) { 1092 if (this.count() >= this.bucketSize) { 1093 var deleteKey = this._keys.splice(0, 1); 1094 this.delete(deleteKey); 1095 } 1096 1097 this._keys.push(key); 1098 this._store[key] = value; 1099 1100 return this._store; 1101 }; 1102 1103 Cache.prototype.get = function(key) { 1104 return this._store[key]; 1105 }; 1106 1107 Cache.prototype.has = function(key) { 1108 return Boolean(this._store[key]); 1109 }; 1110 1111 Cache.prototype.count = function() { 1112 return Object.keys(this._store).length; 1113 }; 1114 1115 Cache.prototype.delete = function(key) { 1116 var exists = Boolean(this._store[key]); 1117 delete this._store[key]; 1118 return exists && !this._store[key]; 1119 }; 1120 1121 function Dispatcher() { 1122 this.events = {}; 1123 } 1124 1125 Dispatcher.prototype.on = function(eventName, callback) { 1126 var event = this.events[eventName]; 1127 if (!event) { 1128 event = new DispatcherEvent(eventName); 1129 this.events[eventName] = event; 1130 } 1131 event.registerCallback(callback); 1132 }; 1133 1134 Dispatcher.prototype.off = function(eventName, callback) { 1135 var event = this.events[eventName]; 1136 if (event && event.callbacks.indexOf(callback) > -1) { 1137 event.unregisterCallback(callback); 1138 if (event.callbacks.length === 0) { 1139 delete this.events[eventName]; 1140 } 1141 } 1142 }; 1143 1144 Dispatcher.prototype.dispatch = function(eventName, payload) { 1145 var event = this.events[eventName]; 1146 if (event) { 1147 event.fire(payload); 1148 } 1149 }; 1150 1151 function DispatcherEvent(eventName) { 1152 this.eventName = eventName; 1153 this.callbacks = []; 1154 } 1155 1156 DispatcherEvent.prototype.registerCallback = function(callback) { 1157 this.callbacks.push(callback); 1158 }; 1159 1160 DispatcherEvent.prototype.unregisterCallback = function(callback) { 1161 var index = this.callbacks.indexOf(callback); 1162 if (index > -1) { 1163 this.callbacks.splice(index, 1); 1164 } 1165 }; 1166 1167 DispatcherEvent.prototype.fire = function(payload) { 1168 var callbacks = this.callbacks.slice(0); 1169 callbacks.forEach(function(callback) { 1170 callback(payload); 1171 }); 1172 }; 1173 1174 function debounce(func, wait) { 1175 var timeout = null; 1176 return function() { 1177 var context = this; 1178 var args = arguments; 1179 clearTimeout(timeout); 1180 timeout = setTimeout(function() { 1181 timeout = null; 1182 func.apply(context, args); 1183 }, wait || 0); 1184 }; 1185 } 1186 1187 function objectToQueryParams(obj, parentKey) { 1188 var output = ''; 1189 parentKey = parentKey || null; 1190 1191 Object.keys(obj).forEach(function(key) { 1192 var outputKey = key + '='; 1193 if (parentKey) { 1194 outputKey = parentKey + '[' + key + ']'; 1195 } 1196 1197 switch (trueTypeOf(obj[key])) { 1198 case 'object': 1199 output += objectToQueryParams(obj[key], parentKey ? outputKey : key); 1200 break; 1201 case 'array': 1202 output += outputKey + '=' + obj[key].join(',') + '&'; 1203 break; 1204 default: 1205 if (parentKey) { 1206 outputKey += '='; 1207 } 1208 output += outputKey + encodeURIComponent(obj[key]) + '&'; 1209 break; 1210 } 1211 }); 1212 1213 return output; 1214 } 1215 1216 function trueTypeOf(obj) { 1217 return Object.prototype.toString 1218 .call(obj) 1219 .slice(8, -1) 1220 .toLowerCase(); 1221 } 1222 1223 var DEBOUNCE_RATE = 10; 1224 var requestDebounced = debounce(request, DEBOUNCE_RATE); 1225 1226 function PredictiveSearch(config) { 1227 if (!config) { 1228 throw new TypeError('No config object was specified'); 1229 } 1230 1231 this._retryAfter = null; 1232 this._currentQuery = null; 1233 1234 this.dispatcher = new Dispatcher(); 1235 this.cache = new Cache({ bucketSize: 40 }); 1236 this.configParams = objectToQueryParams(config); 1237 } 1238 1239 PredictiveSearch.TYPES = { 1240 PRODUCT: 'product', 1241 PAGE: 'page', 1242 ARTICLE: 'article' 1243 }; 1244 1245 PredictiveSearch.FIELDS = { 1246 AUTHOR: 'author', 1247 BODY: 'body', 1248 PRODUCT_TYPE: 'product_type', 1249 TAG: 'tag', 1250 TITLE: 'title', 1251 VARIANTS_BARCODE: 'variants.barcode', 1252 VARIANTS_SKU: 'variants.sku', 1253 VARIANTS_TITLE: 'variants.title', 1254 VENDOR: 'vendor' 1255 }; 1256 1257 PredictiveSearch.UNAVAILABLE_PRODUCTS = { 1258 SHOW: 'show', 1259 HIDE: 'hide', 1260 LAST: 'last' 1261 }; 1262 1263 PredictiveSearch.prototype.query = function query(query) { 1264 try { 1265 validateQuery(query); 1266 } catch (error) { 1267 this.dispatcher.dispatch('error', error); 1268 return; 1269 } 1270 1271 if (query === '') { 1272 return this; 1273 } 1274 1275 this._currentQuery = normalizeQuery(query); 1276 var cacheResult = this.cache.get(this._currentQuery); 1277 if (cacheResult) { 1278 this.dispatcher.dispatch('success', cacheResult); 1279 return this; 1280 } 1281 1282 requestDebounced( 1283 this.configParams, 1284 query, 1285 function(result) { 1286 this.cache.set(normalizeQuery(result.query), result); 1287 if (normalizeQuery(result.query) === this._currentQuery) { 1288 this._retryAfter = null; 1289 this.dispatcher.dispatch('success', result); 1290 } 1291 }.bind(this), 1292 function(error) { 1293 if (error.retryAfter) { 1294 this._retryAfter = error.retryAfter; 1295 } 1296 this.dispatcher.dispatch('error', error); 1297 }.bind(this) 1298 ); 1299 1300 return this; 1301 }; 1302 1303 PredictiveSearch.prototype.on = function on(eventName, callback) { 1304 this.dispatcher.on(eventName, callback); 1305 1306 return this; 1307 }; 1308 1309 PredictiveSearch.prototype.off = function on(eventName, callback) { 1310 this.dispatcher.off(eventName, callback); 1311 1312 return this; 1313 }; 1314 1315 function normalizeQuery(query) { 1316 if (typeof query !== 'string') { 1317 return null; 1318 } 1319 1320 return query 1321 .trim() 1322 .replace(' ', '-') 1323 .toLowerCase(); 1324 } 1325 1326 return PredictiveSearch; 1327})(); 1328 1329this.Shopify = this.Shopify || {}; 1330this.Shopify.theme = this.Shopify.theme || {}; 1331this.Shopify.theme.PredictiveSearchComponent = (function(PredictiveSearch) { 1332 'use strict'; 1333 1334 PredictiveSearch = 1335 PredictiveSearch && PredictiveSearch.hasOwnProperty('default') 1336 ? PredictiveSearch['default'] 1337 : PredictiveSearch; 1338 1339 var DEFAULT_PREDICTIVE_SEARCH_API_CONFIG = { 1340 resources: { 1341 type: [PredictiveSearch.TYPES.PRODUCT], 1342 options: { 1343 unavailable_products: PredictiveSearch.UNAVAILABLE_PRODUCTS.LAST, 1344 fields: [ 1345 PredictiveSearch.FIELDS.TITLE, 1346 PredictiveSearch.FIELDS.VENDOR, 1347 PredictiveSearch.FIELDS.PRODUCT_TYPE, 1348 PredictiveSearch.FIELDS.VARIANTS_TITLE 1349 ] 1350 } 1351 } 1352 }; 1353 1354 function PredictiveSearchComponent(config) { 1355 // validate config 1356 if ( 1357 !config || 1358 !config.selectors || 1359 !config.selectors.input || 1360 !isString(config.selectors.input) || 1361 !config.selectors.result || 1362 !isString(config.selectors.result) || 1363 !config.resultTemplateFct || 1364 !isFunction(config.resultTemplateFct) || 1365 !config.numberOfResultsTemplateFct || 1366 !isFunction(config.numberOfResultsTemplateFct) || 1367 !config.loadingResultsMessageTemplateFct || 1368 !isFunction(config.loadingResultsMessageTemplateFct) 1369 ) { 1370 var error = new TypeError(
1371 'PredictiveSearchComponent config is not valid' 1372 ); 1373 error.type = 'argument'; 1374 throw error; 1375 } 1376 1377 // Find nodes 1378 this.nodes = findNodes(config.selectors); 1379 1380 // Validate nodes 1381 if (!isValidNodes(this.nodes)) { 1382 // eslint-disable-next-line no-console 1383 console.warn('Could not find valid nodes'); 1384 return; 1385 } 1386 1387 // Store the keyword that was used for the search 1388 this._searchKeyword = ''; 1389 1390 // Assign result template 1391 this.resultTemplateFct = config.resultTemplateFct; 1392 1393 // Assign number of results template 1394 this.numberOfResultsTemplateFct = config.numberOfResultsTemplateFct; 1395 1396 // Assign loading state template function 1397 this.loadingResultsMessageTemplateFct = 1398 config.loadingResultsMessageTemplateFct; 1399 1400 // Assign number of search results 1401 this.numberOfResults = config.numberOfResults || 4; 1402 1403 // Set classes 1404 this.classes = { 1405 visibleVariant: config.visibleVariant 1406 ? config.visibleVariant 1407 : 'predictive-search-wrapper--visible', 1408 itemSelected: config.itemSelectedClass 1409 ? config.itemSelectedClass 1410 : 'predictive-search-item--selected', 1411 clearButtonVisible: config.clearButtonVisibleClass 1412 ? config.clearButtonVisibleClass 1413 : 'predictive-search__clear-button--visible' 1414 }; 1415 1416 this.selectors = { 1417 searchResult: config.searchResult 1418 ? config.searchResult 1419 : '[data-search-result]' 1420 }; 1421 1422 // Assign callbacks 1423 this.callbacks = assignCallbacks(config); 1424 1425 // Add input attributes 1426 addInputAttributes(this.nodes.input); 1427 1428 // Add input event listeners 1429 this._addInputEventListeners(); 1430 1431 // Add body listener 1432 this._addBodyEventListener(); 1433 1434 // Add accessibility announcer 1435 this._addAccessibilityAnnouncer(); 1436 1437 // Display the reset button if the input is not empty 1438 this._toggleClearButtonVisibility(); 1439 1440 // Instantiate Predictive Search API 1441 this.predictiveSearch = new PredictiveSearch( 1442 config.PredictiveSearchAPIConfig 1443 ? config.PredictiveSearchAPIConfig 1444 : DEFAULT_PREDICTIVE_SEARCH_API_CONFIG 1445 ); 1446 1447 // Add predictive search success event listener 1448 this.predictiveSearch.on( 1449 'success', 1450 this._handlePredictiveSearchSuccess.bind(this) 1451 ); 1452 1453 // Add predictive search error event listener 1454 this.predictiveSearch.on( 1455 'error', 1456 this._handlePredictiveSearchError.bind(this) 1457 ); 1458 } 1459 1460 /** 1461 * Private methods 1462 */ 1463 function findNodes(selectors) { 1464 return { 1465 input: document.querySelector(selectors.input), 1466 reset: document.querySelector(selectors.reset), 1467 result: document.querySelector(selectors.result) 1468 }; 1469 } 1470 1471 function isValidNodes(nodes) { 1472 if ( 1473 !nodes || 1474 !nodes.input || 1475 !nodes.result || 1476 nodes.input.tagName !== 'INPUT' 1477 ) { 1478 return false; 1479 } 1480 1481 return true; 1482 } 1483 1484 function assignCallbacks(config) { 1485 return { 1486 onBodyMousedown: config.onBodyMousedown, 1487 onBeforeOpen: config.onBeforeOpen, 1488 onOpen: config.onOpen, 1489 onBeforeClose: config.onBeforeClose, 1490 onClose: config.onClose, 1491 onInputFocus: config.onInputFocus, 1492 onInputKeyup: config.onInputKeyup, 1493 onInputBlur: config.onInputBlur, 1494 onInputReset: config.onInputReset, 1495 onBeforeDestroy: config.onBeforeDestroy, 1496 onDestroy: config.onDestroy 1497 }; 1498 } 1499 1500 function addInputAttributes(input) { 1501 input.setAttribute('autocorrect', 'off'); 1502 input.setAttribute('autocomplete', 'off'); 1503 input.setAttribute('autocapitalize', 'off'); 1504 input.setAttribute('spellcheck', 'false'); 1505 } 1506 1507 function removeInputAttributes(input) { 1508 input.removeAttribute('autocorrect', 'off'); 1509 input.removeAttribute('autocomplete', 'off'); 1510 input.removeAttribute('autocapitalize', 'off'); 1511 input.removeAttribute('spellcheck', 'false'); 1512 } 1513 1514 /** 1515 * Public variables 1516 */ 1517 PredictiveSearchComponent.prototype.isResultVisible = false; 1518 PredictiveSearchComponent.prototype.results = {}; 1519 1520 /** 1521 * "Private" variables 1522 */ 1523 PredictiveSearchComponent.prototype._latencyTimer = null; 1524 PredictiveSearchComponent.prototype._resultNodeClicked = false;
1525 1526 /** 1527 * "Private" instance methods 1528 */ 1529 PredictiveSearchComponent.prototype._addInputEventListeners = function() { 1530 var input = this.nodes.input; 1531 var reset = this.nodes.reset; 1532 1533 if (!input) { 1534 return; 1535 } 1536 1537 this._handleInputFocus = this._handleInputFocus.bind(this); 1538 this._handleInputBlur = this._handleInputBlur.bind(this); 1539 this._handleInputKeyup = this._handleInputKeyup.bind(this); 1540 this._handleInputKeydown = this._handleInputKeydown.bind(this); 1541 1542 input.addEventListener('focus', this._handleInputFocus); 1543 input.addEventListener('blur', this._handleInputBlur); 1544 input.addEventListener('keyup', this._handleInputKeyup); 1545 input.addEventListener('keydown', this._handleInputKeydown); 1546 1547 if (reset) { 1548 this._handleInputReset = this._handleInputReset.bind(this); 1549 reset.addEventListener('click', this._handleInputReset); 1550 } 1551 }; 1552 1553 PredictiveSearchComponent.prototype._removeInputEventListeners = function() { 1554 var input = this.nodes.input; 1555 1556 input.removeEventListener('focus', this._handleInputFocus); 1557 input.removeEventListener('blur', this._handleInputBlur); 1558 input.removeEventListener('keyup', this._handleInputKeyup); 1559 input.removeEventListener('keydown', this._handleInputKeydown); 1560 }; 1561 1562 PredictiveSearchComponent.prototype._addBodyEventListener = function() { 1563 this._handleBodyMousedown = this._handleBodyMousedown.bind(this); 1564 1565 document 1566 .querySelector('body') 1567 .addEventListener('mousedown', this._handleBodyMousedown); 1568 }; 1569 1570 PredictiveSearchComponent.prototype._removeBodyEventListener = function() { 1571 document 1572 .querySelector('body') 1573 .removeEventListener('mousedown', this._handleBodyMousedown); 1574 }; 1575 1576 PredictiveSearchComponent.prototype._removeClearButtonEventListener = function() { 1577 var reset = this.nodes.reset; 1578 1579 if (!reset) { 1580 return; 1581 } 1582 1583 reset.removeEventListener('click', this._handleInputReset); 1584 }; 1585 1586 /** 1587 * Event handlers 1588 */ 1589 PredictiveSearchComponent.prototype._handleBodyMousedown = function(evt) { 1590 if (this.isResultVisible && this.nodes !== null) { 1591 if ( 1592 evt.target.isEqualNode(this.nodes.input) || 1593 this.nodes.input.contains(evt.target) || 1594 evt.target.isEqualNode(this.nodes.result) || 1595 this.nodes.result.contains(evt.target) 1596 ) { 1597 this._resultNodeClicked = true; 1598 } else { 1599 if (isFunction(this.callbacks.onBodyMousedown)) { 1600 var returnedValue = this.callbacks.onBodyMousedown(this.nodes); 1601 if (isBoolean(returnedValue) && returnedValue) { 1602 this.close(); 1603 } 1604 } else { 1605 this.close(); 1606 } 1607 } 1608 } 1609 }; 1610 1611 PredictiveSearchComponent.prototype._handleInputFocus = function(evt) { 1612 if (isFunction(this.callbacks.onInputFocus)) { 1613 var returnedValue = this.callbacks.onInputFocus(this.nodes); 1614 if (isBoolean(returnedValue) && !returnedValue) { 1615 return false; 1616 } 1617 } 1618 1619 if (evt.target.value.length > 0) { 1620 this._search(); 1621 } 1622 1623 return true; 1624 }; 1625 1626 PredictiveSearchComponent.prototype._handleInputBlur = function() { 1627 // This has to be done async, to wait for the focus to be on the next 1628 // element and avoid closing the results. 1629 // Example: Going from the input to the reset button. 1630 setTimeout( 1631 function() { 1632 if (isFunction(this.callbacks.onInputBlur)) { 1633 var returnedValue = this.callbacks.onInputBlur(this.nodes); 1634 if (isBoolean(returnedValue) && !returnedValue) { 1635 return false; 1636 } 1637 } 1638 1639 if (document.activeElement.isEqualNode(this.nodes.reset)) { 1640 return false; 1641 } 1642 1643 if (this._resultNodeClicked) { 1644 this._resultNodeClicked = false; 1645 return false; 1646 } 1647 1648 this.close(); 1649 }.bind(this) 1650 ); 1651 1652 return true; 1653 }; 1654 1655 PredictiveSearchComponent.prototype._addAccessibilityAnnouncer = function() { 1656 this._accessibilityAnnouncerDiv = window.document.createElement('div'); 1657 1658 this._accessibilityAnnouncerDiv.setAttribute( 1659 'style', 1660 'position: absolute !important; overflow: hidden; clip: rect(0 0 0 0); height: 1px; width: 1px; margin: -1px; padding: 0; border: 0;' 1661 ); 1662 1663 this._accessibilityAnnouncerDiv.setAttribute('data-search-announcer', ''); 1664 this._accessibilityAnnouncerDiv.setAttribute('aria-live', 'polite'); 1665 this._accessibilityAnnouncerDiv.setAttribute('aria-atomic', 'true'); 1666 1667 this.nodes.result.parentElement.appendChild( 1668 this._accessibilityAnnouncerDiv 1669 ); 1670 }; 1671 1672 PredictiveSearchComponent.prototype._removeAccessibilityAnnouncer = function() { 1673 this.nodes.result.parentElement.removeChild( 1674 this._accessibilityAnnouncerDiv 1675 ); 1676 }; 1677 1678 PredictiveSearchComponent.prototype._updateAccessibilityAttributesAfterSelectingElement = function( 1679 previousSelectedElement, 1680 currentSelectedElement 1681 ) { 1682 // Update the active descendant on the search input 1683 this.nodes.input.setAttribute( 1684 'aria-activedescendant', 1685 currentSelectedElement.id 1686 ); 1687 1688 // Unmark the previousSelected elemented as selected 1689 if (previousSelectedElement) { 1690 previousSelectedElement.removeAttribute('aria-selected'); 1691 } 1692 1693 // Mark the element as selected 1694 currentSelectedElement.setAttribute('aria-selected', true); 1695 }; 1696 1697 PredictiveSearchComponent.prototype._clearAriaActiveDescendant = function() { 1698 this.nodes.input.setAttribute('aria-activedescendant', ''); 1699 }; 1700 1701 PredictiveSearchComponent.prototype._announceNumberOfResultsFound = function( 1702 results 1703 ) { 1704 var currentAnnouncedMessage = this._accessibilityAnnouncerDiv.innerHTML; 1705 var newMessage = this.numberOfResultsTemplateFct(results); 1706 1707 // If the messages are the same, they won't get announced 1708 // add white space so it gets announced 1709 if (currentAnnouncedMessage === newMessage) { 1710 newMessage = newMessage + ' '; 1711 } 1712 1713 this._accessibilityAnnouncerDiv.innerHTML = newMessage; 1714 }; 1715
1716 PredictiveSearchComponent.prototype._announceLoadingState = function() { 1717 this._accessibilityAnnouncerDiv.innerHTML = this.loadingResultsMessageTemplateFct(); 1718 }; 1719 1720 PredictiveSearchComponent.prototype._handleInputKeyup = function(evt) { 1721 var UP_ARROW_KEY_CODE = 38; 1722 var DOWN_ARROW_KEY_CODE = 40; 1723 var RETURN_KEY_CODE = 13; 1724 var ESCAPE_KEY_CODE = 27; 1725 1726 if (isFunction(this.callbacks.onInputKeyup)) { 1727 var returnedValue = this.callbacks.onInputKeyup(this.nodes); 1728 if (isBoolean(returnedValue) && !returnedValue) { 1729 return false; 1730 } 1731 } 1732 1733 this._toggleClearButtonVisibility(); 1734 1735 if (this.isResultVisible && this.nodes !== null) { 1736 if (evt.keyCode === UP_ARROW_KEY_CODE) { 1737 this._navigateOption(evt, 'UP'); 1738 return true; 1739 } 1740 1741 if (evt.keyCode === DOWN_ARROW_KEY_CODE) { 1742 this._navigateOption(evt, 'DOWN'); 1743 return true; 1744 } 1745 1746 if (evt.keyCode === RETURN_KEY_CODE) { 1747 this._selectOption(); 1748 return true; 1749 } 1750 1751 if (evt.keyCode === ESCAPE_KEY_CODE) { 1752 this.close(); 1753 } 1754 } 1755 1756 if (evt.target.value.length <= 0) { 1757 this.close(); 1758 this._setKeyword(''); 1759 } else if (evt.target.value.length > 0) { 1760 this._search(); 1761 } 1762 1763 return true; 1764 }; 1765 1766 PredictiveSearchComponent.prototype._handleInputKeydown = function(evt) { 1767 var RETURN_KEY_CODE = 13; 1768 var UP_ARROW_KEY_CODE = 38; 1769 var DOWN_ARROW_KEY_CODE = 40; 1770 1771 // Prevent the form default submission if there is a selected option 1772 if (evt.keyCode === RETURN_KEY_CODE && this._getSelectedOption() !== null) { 1773 evt.preventDefault(); 1774 } 1775 1776 // Prevent the cursor from moving in the input when using the up and down arrow keys 1777 if ( 1778 evt.keyCode === UP_ARROW_KEY_CODE || 1779 evt.keyCode === DOWN_ARROW_KEY_CODE 1780 ) { 1781 evt.preventDefault(); 1782 } 1783 }; 1784 1785 PredictiveSearchComponent.prototype._handleInputReset = function(evt) { 1786 evt.preventDefault(); 1787 1788 if (isFunction(this.callbacks.onInputReset)) { 1789 var returnedValue = this.callbacks.onInputReset(this.nodes); 1790 if (isBoolean(returnedValue) && !returnedValue) { 1791 return false; 1792 } 1793 } 1794 1795 this.nodes.input.value = ''; 1796 this.nodes.input.focus(); 1797 this._toggleClearButtonVisibility(); 1798 this.close(); 1799 1800 return true; 1801 }; 1802 1803 PredictiveSearchComponent.prototype._navigateOption = function( 1804 evt, 1805 direction 1806 ) { 1807 var currentOption = this._getSelectedOption(); 1808 1809 if (!currentOption) { 1810 var firstOption = this.nodes.result.querySelector( 1811 this.selectors.searchResult 1812 ); 1813 firstOption.classList.add(this.classes.itemSelected); 1814 this._updateAccessibilityAttributesAfterSelectingElement( 1815 null, 1816 firstOption 1817 ); 1818 } else { 1819 if (direction === 'DOWN') { 1820 var nextOption = currentOption.nextElementSibling; 1821 if (nextOption) { 1822 currentOption.classList.remove(this.classes.itemSelected); 1823 nextOption.classList.add(this.classes.itemSelected); 1824 this._updateAccessibilityAttributesAfterSelectingElement( 1825 currentOption, 1826 nextOption 1827 ); 1828 } 1829 } else { 1830 var previousOption = currentOption.previousElementSibling; 1831 if (previousOption) { 1832 currentOption.classList.remove(this.classes.itemSelected); 1833 previousOption.classList.add(this.classes.itemSelected); 1834 this._updateAccessibilityAttributesAfterSelectingElement( 1835 currentOption, 1836 previousOption 1837 ); 1838 } 1839 } 1840 } 1841 }; 1842 1843 PredictiveSearchComponent.prototype._getSelectedOption = function() { 1844 return this.nodes.result.querySelector('.' + this.classes.itemSelected); 1845 }; 1846 1847 PredictiveSearchComponent.prototype._selectOption = function() { 1848 var selectedOption = this._getSelectedOption(); 1849 1850 if (selectedOption) { 1851 selectedOption.querySelector('a, button').click(); 1852 } 1853 }; 1854 1855 PredictiveSearchComponent.prototype._search = function() { 1856 var newSearchKeyword = this.nodes.input.value; 1857 1858 if (this._searchKeyword === newSearchKeyword) { 1859 return; 1860 } 1861 1862 clearTimeout(this._latencyTimer); 1863 this._latencyTimer = setTimeout( 1864 function() { 1865 this.results.isLoading = true; 1866 1867 // Annonuce that we're loading the results
1868 this._announceLoadingState(); 1869 1870 this.nodes.result.classList.add(this.classes.visibleVariant); 1871 // NOTE: We could benifit in using DOMPurify. 1872 // https://github.com/cure53/DOMPurify 1873 this.nodes.result.innerHTML = this.resultTemplateFct(this.results); 1874 }.bind(this), 1875 500 1876 ); 1877 1878 this.predictiveSearch.query(newSearchKeyword); 1879 this._setKeyword(newSearchKeyword); 1880 }; 1881 1882 PredictiveSearchComponent.prototype._handlePredictiveSearchSuccess = function( 1883 json 1884 ) { 1885 clearTimeout(this._latencyTimer); 1886 this.results = json.resources.results; 1887 1888 this.results.isLoading = false; 1889 this.results.products = this.results.products.slice( 1890 0, 1891 this.numberOfResults 1892 ); 1893 this.results.canLoadMore = 1894 this.numberOfResults <= this.results.products.length; 1895 this.results.searchQuery = this.nodes.input.value; 1896 1897 if (this.results.products.length > 0 || this.results.searchQuery) { 1898 this.nodes.result.innerHTML = this.resultTemplateFct(this.results); 1899 this._announceNumberOfResultsFound(this.results); 1900 this.open(); 1901 } else { 1902 this.nodes.result.innerHTML = ''; 1903 1904 this._closeOnNoResults(); 1905 } 1906 }; 1907 1908 PredictiveSearchComponent.prototype._handlePredictiveSearchError = function() { 1909 clearTimeout(this._latencyTimer); 1910 this.nodes.result.innerHTML = ''; 1911 1912 this._closeOnNoResults(); 1913 }; 1914 1915 PredictiveSearchComponent.prototype._closeOnNoResults = function() { 1916 if (this.nodes) { 1917 this.nodes.result.classList.remove(this.classes.visibleVariant); 1918 } 1919 1920 this.isResultVisible = false; 1921 }; 1922 1923 PredictiveSearchComponent.prototype._setKeyword = function(keyword) { 1924 this._searchKeyword = keyword; 1925 }; 1926 1927 PredictiveSearchComponent.prototype._toggleClearButtonVisibility = function() { 1928 if (!this.nodes.reset) { 1929 return; 1930 } 1931 1932 if (this.nodes.input.value.length > 0) { 1933 this.nodes.reset.classList.add(this.classes.clearButtonVisible); 1934 } else { 1935 this.nodes.reset.classList.remove(this.classes.clearButtonVisible); 1936 } 1937 }; 1938 1939 /** 1940 * Public methods 1941 */ 1942 PredictiveSearchComponent.prototype.open = function() { 1943 if (this.isResultVisible) { 1944 return; 1945 } 1946 1947 if (isFunction(this.callbacks.onBeforeOpen)) { 1948 var returnedValue = this.callbacks.onBeforeOpen(this.nodes); 1949 if (isBoolean(returnedValue) && !returnedValue) { 1950 return false; 1951 } 1952 } 1953 1954 this.nodes.result.classList.add(this.classes.visibleVariant); 1955 this.nodes.input.setAttribute('aria-expanded', true); 1956 this.isResultVisible = true; 1957 1958 if (isFunction(this.callbacks.onOpen)) { 1959 return this.callbacks.onOpen(this.nodes) || true; 1960 } 1961 1962 return true; 1963 }; 1964 1965 PredictiveSearchComponent.prototype.close = function() { 1966 if (!this.isResultVisible) { 1967 return true; 1968 } 1969 1970 if (isFunction(this.callbacks.onBeforeClose)) { 1971 var returnedValue = this.callbacks.onBeforeClose(this.nodes); 1972 if (isBoolean(returnedValue) && !returnedValue) { 1973 return false; 1974 } 1975 } 1976 1977 if (this.nodes) { 1978 this.nodes.result.classList.remove(this.classes.visibleVariant); 1979 } 1980 1981 this.nodes.input.setAttribute('aria-expanded', false); 1982 this._clearAriaActiveDescendant(); 1983 this._setKeyword(''); 1984 1985 if (isFunction(this.callbacks.onClose)) { 1986 this.callbacks.onClose(this.nodes); 1987 } 1988 1989 this.isResultVisible = false; 1990 this.results = {}; 1991 1992 return true; 1993 }; 1994 1995 PredictiveSearchComponent.prototype.destroy = function() { 1996 this.close(); 1997 1998 if (isFunction(this.callbacks.onBeforeDestroy)) { 1999 var returnedValue = this.callbacks.onBeforeDestroy(this.nodes); 2000 if (isBoolean(returnedValue) && !returnedValue) { 2001 return false; 2002 } 2003 } 2004 2005 this.nodes.result.classList.remove(this.classes.visibleVariant); 2006 removeInputAttributes(this.nodes.input); 2007 this._removeInputEventListeners(); 2008 this._removeBodyEventListener(); 2009 this._removeAccessibilityAnnouncer(); 2010 this._removeClearButtonEventListener(); 2011 2012 if (isFunction(this.callbacks.onDestroy)) { 2013 this.callbacks.onDestroy(this.nodes); 2014 } 2015 2016 return true; 2017 }; 2018 2019 PredictiveSearchComponent.prototype.clearAndClose = function() { 2020 this.nodes.input.value = ''; 2021 this.close(); 2022 }; 2023 2024 /** 2025 * Utilities 2026 */ 2027 function getTypeOf(value) { 2028 return Object.prototype.toString.call(value); 2029 } 2030 2031 function isString(value) { 2032 return getTypeOf(value) === '[object String]'; 2033 } 2034 2035 function isBoolean(value) { 2036 return getTypeOf(value) === '[object Boolean]'; 2037 } 2038 2039 function isFunction(value) { 2040 return getTypeOf(value) === '[object Function]'; 2041 } 2042 2043 return PredictiveSearchComponent; 2044})(Shopify.theme.PredictiveSearch); 2045 2046 2047/* ================ GLOBAL ================ */ 2048/*============================================================================ 2049 Drawer modules 2050==============================================================================*/ 2051theme.Drawers = (function() { 2052 function Drawer(id, position, options) { 2053 var DEFAULT_OPEN_CLASS = 'js-drawer-open'; 2054 var DEFAULT_CLOSE_CLASS = 'js-drawer-close'; 2055 2056 var defaults = { 2057 selectors: { 2058 openVariant: '.' + DEFAULT_OPEN_CLASS + '-' + position, 2059 close: '.' + DEFAULT_CLOSE_CLASS 2060 }, 2061 classes: { 2062 open: DEFAULT_OPEN_CLASS,
2063 openVariant: DEFAULT_OPEN_CLASS + '-' + position 2064 }, 2065 withPredictiveSearch: false 2066 }; 2067 2068 this.nodes = { 2069 $parent: $('html').add('body'), 2070 $page: $('#PageContainer') 2071 }; 2072 2073 this.config = $.extend(defaults, options); 2074 this.position = position; 2075 this.$drawer = $('#' + id); 2076 2077 if (!this.$drawer.length) { 2078 return false; 2079 } 2080 2081 this.drawerIsOpen = false; 2082 this.init(); 2083 } 2084 2085 Drawer.prototype.init = function() { 2086 $(this.config.selectors.openVariant).on('click', $.proxy(this.open, this)); 2087 this.$drawer.on( 2088 'click', 2089 this.config.selectors.close, 2090 $.proxy(this.close, this) 2091 ); 2092 }; 2093 2094 Drawer.prototype.open = function(evt) { 2095 // Keep track if drawer was opened from a click, or called by another function 2096 var externalCall = false; 2097 2098 // Prevent following href if link is clicked 2099 if (evt) { 2100 evt.preventDefault(); 2101 } else { 2102 externalCall = true; 2103 } 2104 2105 // Without this, the drawer opens, the click event bubbles up to nodes.$page 2106 // which closes the drawer. 2107 if (evt && evt.stopPropagation) { 2108 evt.stopPropagation(); 2109 // save the source of the click, we'll focus to this on close 2110 this.$activeSource = $(evt.currentTarget); 2111 } 2112 2113 if (this.drawerIsOpen && !externalCall) { 2114 return this.close(); 2115 } 2116 2117 // Add is-transitioning class to moved elements on open so drawer can have 2118 // transition for close animation 2119 if (!this.config.withPredictiveSearch) { 2120 this.$drawer.prepareTransition(); 2121 } 2122 2123 this.nodes.$parent.addClass( 2124 this.config.classes.open + ' ' + this.config.classes.openVariant 2125 ); 2126 this.drawerIsOpen = true; 2127 2128 // Run function when draw opens if set 2129 if ( 2130 this.config.onDrawerOpen && 2131 typeof this.config.onDrawerOpen === 'function' 2132 ) { 2133 if (!externalCall) { 2134 this.config.onDrawerOpen(); 2135 } 2136 } 2137 2138 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { 2139 this.$activeSource.attr('aria-expanded', 'true'); 2140 } 2141 2142 // Set focus on drawer 2143 var trapFocusConfig = { 2144 $container: this.$drawer, 2145 namespace: 'drawer_focus' 2146 }; 2147 if (this.config.$elementToFocusOnOpen) { 2148 trapFocusConfig.$elementToFocus = this.config.$elementToFocusOnOpen; 2149 } 2150 2151 slate.a11y.trapFocus(trapFocusConfig); 2152 2153 this.bindEvents(); 2154 2155 return this; 2156 }; 2157 2158 Drawer.prototype.close = function() { 2159 if (!this.drawerIsOpen) { 2160 // don't close a closed drawer 2161 return; 2162 } 2163 2164 // deselect any focused form elements 2165 $(document.activeElement).trigger('blur'); 2166 2167 // Ensure closing transition is applied to moved elements, like the nav 2168 if (!this.config.withPredictiveSearch) { 2169 this.$drawer.prepareTransition(); 2170 } 2171 2172 this.nodes.$parent.removeClass( 2173 this.config.classes.open + ' ' + this.config.classes.openVariant 2174 ); 2175 2176 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { 2177 this.$activeSource.attr('aria-expanded', 'false'); 2178 } 2179 2180 this.drawerIsOpen = false; 2181 2182 // Remove focus on drawer 2183 slate.a11y.removeTrapFocus({ 2184 $container: this.$drawer, 2185 namespace: 'drawer_focus' 2186 }); 2187 2188 this.unbindEvents(); 2189 2190 // Run function when draw closes if set 2191 if ( 2192 this.config.onDrawerClose && 2193 typeof this.config.onDrawerClose === 'function' 2194 ) { 2195 this.config.onDrawerClose(); 2196 } 2197 }; 2198 2199 Drawer.prototype.bindEvents = function() { 2200 this.nodes.$parent.on( 2201 'keyup.drawer', 2202 $.proxy(function(evt) { 2203 // close on 'esc' keypress 2204 if (evt.keyCode === 27) { 2205 this.close(); 2206 return false; 2207 } else { 2208 return true; 2209 } 2210 }, this) 2211 ); 2212 2213 // Lock scrolling on mobile 2214 this.nodes.$page.on('touchmove.drawer', function() { 2215 return false; 2216 }); 2217 2218 this.nodes.$page.on( 2219 'click.drawer', 2220 $.proxy(function() { 2221 this.close(); 2222 return false; 2223 }, this) 2224 ); 2225 }; 2226 2227 Drawer.prototype.unbindEvents = function() { 2228 this.nodes.$page.off('.drawer'); 2229 this.nodes.$parent.off('.drawer'); 2230 }; 2231 2232 return Drawer; 2233})(); 2234 2235theme.Helpers = (function() { 2236 var touchDevice = false;
2237 2238 function setTouch() { 2239 touchDevice = true; 2240 } 2241 2242 function isTouch() { 2243 return touchDevice; 2244 } 2245 return { 2246 setTouch: setTouch, 2247 isTouch: isTouch 2248 }; 2249})(); 2250 2251theme.LibraryLoader = (function() { 2252 var types = { 2253 link: 'link', 2254 script: 'script' 2255 }; 2256 2257 var status = { 2258 requested: 'requested', 2259 loaded: 'loaded' 2260 }; 2261 2262 var cloudCdn = 'https://cdn.shopify.com/shopifycloud/'; 2263 2264 var libraries = { 2265 youtubeSdk: { 2266 tagId: 'youtube-sdk', 2267 src: 'https://www.youtube.com/iframe_api', 2268 type: types.script 2269 }, 2270 plyrShopifyStyles: { 2271 tagId: 'plyr-shopify-styles', 2272 src: cloudCdn + 'shopify-plyr/v1.0/shopify-plyr.css', 2273 type: types.link 2274 }, 2275 modelViewerUiStyles: { 2276 tagId: 'shopify-model-viewer-ui-styles', 2277 src: cloudCdn + 'model-viewer-ui/assets/v1.0/model-viewer-ui.css', 2278 type: types.link 2279 } 2280 }; 2281 2282 function load(libraryName, callback) { 2283 var library = libraries[libraryName]; 2284 2285 if (!library) return; 2286 if (library.status === status.requested) return; 2287 2288 callback = callback || function() {}; 2289 if (library.status === status.loaded) { 2290 callback(); 2291 return; 2292 } 2293 2294 library.status = status.requested; 2295 2296 var tag; 2297 2298 switch (library.type) { 2299 case types.script: 2300 tag = createScriptTag(library, callback); 2301 break; 2302 case types.link: 2303 tag = createLinkTag(library, callback); 2304 break; 2305 } 2306 2307 tag.id = library.tagId; 2308 library.element = tag; 2309 2310 var firstScriptTag = document.getElementsByTagName(library.type)[0]; 2311 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 2312 } 2313 2314 function createScriptTag(library, callback) { 2315 var tag = document.createElement('script'); 2316 tag.src = library.src; 2317 tag.addEventListener('load', function() { 2318 library.status = status.loaded; 2319 callback(); 2320 }); 2321 return tag; 2322 } 2323 2324 function createLinkTag(library, callback) { 2325 var tag = document.createElement('link'); 2326 tag.href = library.src; 2327 tag.rel = 'stylesheet'; 2328 tag.type = 'text/css'; 2329 tag.addEventListener('load', function() { 2330 library.status = status.loaded; 2331 callback(); 2332 }); 2333 return tag; 2334 } 2335 2336 return { 2337 load: load 2338 }; 2339})(); 2340 2341 2342/* ================ MODULES ================ */ 2343window.theme = window.theme || {}; 2344 2345theme.Header = (function() { 2346 var selectors = { 2347 body: 'body', 2348 navigation: '#AccessibleNav', 2349 siteNavHasDropdown: '[data-has-dropdowns]', 2350 siteNavChildLinks: '.site-nav__child-link', 2351 siteNavActiveDropdown: '.site-nav--active-dropdown', 2352 siteNavHasCenteredDropdown: '.site-nav--has-centered-dropdown', 2353 siteNavCenteredDropdown: '.site-nav__dropdown--centered', 2354 siteNavLinkMain: '.site-nav__link--main', 2355 siteNavChildLink: '.site-nav__link--last', 2356 siteNavDropdown: '.site-nav__dropdown', 2357 siteHeader: '.site-header' 2358 }; 2359 2360 var config = { 2361 activeClass: 'site-nav--active-dropdown', 2362 childLinkClass: 'site-nav__child-link', 2363 rightDropdownClass: 'site-nav__dropdown--right', 2364 leftDropdownClass: 'site-nav__dropdown--left' 2365 }; 2366 2367 var cache = {}; 2368 2369 function init() { 2370 cacheSelectors(); 2371 styleDropdowns($(selectors.siteNavHasDropdown)); 2372 positionFullWidthDropdowns(); 2373 2374 cache.$parents.on('click.siteNav', function() { 2375 var $el = $(this); 2376 $el.hasClass(config.activeClass) ? hideDropdown($el) : showDropdown($el); 2377 }); 2378 2379 // check when we're leaving a dropdown and close the active dropdown 2380 $(selectors.siteNavChildLink).on('focusout.siteNav', function() { 2381 setTimeout(function() { 2382 if ( 2383 $(document.activeElement).hasClass(config.childLinkClass) || 2384 !cache.$activeDropdown.length 2385 ) { 2386 return; 2387 } 2388 2389 hideDropdown(cache.$activeDropdown); 2390 }); 2391 }); 2392 2393 // close dropdowns when on top level nav 2394 cache.$topLevel.on('focus.siteNav', function() { 2395 if (cache.$activeDropdown.length) { 2396 hideDropdown(cache.$activeDropdown); 2397 } 2398 }); 2399 2400 cache.$subMenuLinks.on('click.siteNav', function(evt) { 2401 // Prevent click on body from firing instead of link 2402 evt.stopImmediatePropagation(); 2403 }); 2404 2405 $(window).resize( 2406 $.debounce(50, function() { 2407 styleDropdowns($(selectors.siteNavHasDropdown)); 2408 positionFullWidthDropdowns(); 2409 }) 2410 ); 2411 } 2412 2413 function cacheSelectors() { 2414 cache = { 2415 $nav: $(selectors.navigation), 2416 $topLevel: $(selectors.siteNavLinkMain), 2417 $parents: $(selectors.navigation).find(selectors.siteNavHasDropdown), 2418 $subMenuLinks: $(selectors.siteNavChildLinks), 2419 $activeDropdown: $(selectors.siteNavActiveDropdown), 2420 $siteHeader: $(selectors.siteHeader) 2421 }; 2422 } 2423 2424 function showDropdown($el) { 2425 $el.addClass(config.activeClass); 2426 2427 // close open dropdowns 2428 if (cache.$activeDropdown.length) { 2429 hideDropdown(cache.$activeDropdown); 2430 } 2431 2432 cache.$activeDropdown = $el; 2433 2434 // set expanded on open dropdown 2435 $el.find(selectors.siteNavLinkMain).attr('aria-expanded', 'true'); 2436 2437 setTimeout(function() { 2438 $(window).on('keyup.siteNav', function(evt) { 2439 if (evt.keyCode === 27) { 2440 hideDropdown($el); 2441 } 2442 }); 2443 2444 $(selectors.body).on('click.siteNav', function() { 2445 hideDropdown($el); 2446 }); 2447 }, 250); 2448 } 2449 2450 function hideDropdown($el) { 2451 // remove aria on open dropdown 2452 $el.find(selectors.siteNavLinkMain).attr('aria-expanded', 'false'); 2453 $el.removeClass(config.activeClass); 2454 2455 // reset active dropdown 2456 cache.$activeDropdown = $(selectors.siteNavActiveDropdown); 2457 2458 $(selectors.body).off('click.siteNav'); 2459 $(window).off('keyup.siteNav'); 2460 } 2461 2462 function styleDropdowns($dropdownListItems) { 2463 $dropdownListItems.each(function() {
2464 var $dropdownLi = $(this).find(selectors.siteNavDropdown); 2465 if (!$dropdownLi.length) { 2466 return; 2467 } 2468 var isRightOfLogo = 2469 Math.ceil($(this).offset().left) > 2470 Math.floor(cache.$siteHeader.outerWidth()) / 2 2471 ? true 2472 : false; 2473 if (isRightOfLogo) { 2474 $dropdownLi 2475 .removeClass(config.leftDropdownClass) 2476 .addClass(config.rightDropdownClass); 2477 } else { 2478 $dropdownLi 2479 .removeClass(config.rightDropdownClass) 2480 .addClass(config.leftDropdownClass); 2481 } 2482 }); 2483 } 2484 2485 function positionFullWidthDropdowns() { 2486 var $listWithCenteredDropdown = $(selectors.siteNavHasCenteredDropdown); 2487 2488 $listWithCenteredDropdown.each(function() { 2489 var $hasCenteredDropdown = $(this); 2490 var $fullWidthDropdown = $hasCenteredDropdown.find( 2491 selectors.siteNavCenteredDropdown 2492 ); 2493 2494 var fullWidthDropdownOffset = $hasCenteredDropdown.position().top + 41; 2495 $fullWidthDropdown.css('top', fullWidthDropdownOffset); 2496 }); 2497 } 2498 2499 function unload() { 2500 $(window).off('.siteNav'); 2501 cache.$parents.off('.siteNav'); 2502 cache.$subMenuLinks.off('.siteNav'); 2503 cache.$topLevel.off('.siteNav'); 2504 $(selectors.siteNavChildLink).off('.siteNav'); 2505 $(selectors.body).off('.siteNav'); 2506 } 2507 2508 return { 2509 init: init, 2510 unload: unload 2511 }; 2512})(); 2513 2514window.theme = window.theme || {}; 2515 2516theme.MobileNav = (function() { 2517 var classes = { 2518 mobileNavOpenIcon: 'mobile-nav--open', 2519 mobileNavCloseIcon: 'mobile-nav--close', 2520 navLinkWrapper: 'mobile-nav__item', 2521 navLink: 'mobile-nav__link', 2522 subNavLink: 'mobile-nav__sublist-link', 2523 return: 'mobile-nav__return-btn', 2524 subNavActive: 'is-active', 2525 subNavClosing: 'is-closing', 2526 navOpen: 'js-menu--is-open', 2527 subNavShowing: 'sub-nav--is-open', 2528 thirdNavShowing: 'third-nav--is-open', 2529 subNavToggleBtn: 'js-toggle-submenu' 2530 }; 2531 var cache = {}; 2532 var isTransitioning; 2533 var $activeSubNav; 2534 var $activeTrigger; 2535 var menuLevel = 1; 2536 // Breakpoints from src/stylesheets/global/variables.scss.liquid 2537 var mediaQuerySmall = 'screen and (max-width: 749px)'; 2538 2539 function init() { 2540 cacheSelectors(); 2541 2542 cache.$mobileNavToggle.on('click', toggleMobileNav); 2543 cache.$subNavToggleBtn.on('click.subNav', toggleSubNav); 2544 2545 // Close mobile nav when unmatching mobile breakpoint 2546 enquire.register(mediaQuerySmall, { 2547 unmatch: function() {
2548 if (cache.$mobileNavContainer.hasClass(classes.navOpen)) { 2549 closeMobileNav(); 2550 } 2551 } 2552 }); 2553 } 2554 2555 function toggleMobileNav() { 2556 if (cache.$mobileNavToggle.hasClass(classes.mobileNavCloseIcon)) { 2557 closeMobileNav(); 2558 } else { 2559 openMobileNav(); 2560 } 2561 } 2562 2563 function cacheSelectors() { 2564 cache = { 2565 $pageContainer: $('#PageContainer'), 2566 $siteHeader: $('.site-header'), 2567 $mobileNavToggle: $('.js-mobile-nav-toggle'), 2568 $mobileNavContainer: $('.mobile-nav-wrapper'), 2569 $mobileNav: $('#MobileNav'), 2570 $sectionHeader: $('#shopify-section-header'), 2571 $subNavToggleBtn: $('.' + classes.subNavToggleBtn) 2572 }; 2573 } 2574 2575 function openMobileNav() { 2576 var translateHeaderHeight = cache.$siteHeader.outerHeight(); 2577 2578 cache.$mobileNavContainer.prepareTransition().addClass(classes.navOpen); 2579 2580 cache.$mobileNavContainer.css({ 2581 transform: 'translateY(' + translateHeaderHeight + 'px)' 2582 }); 2583 2584 cache.$pageContainer.css({ 2585 transform: 2586 'translate3d(0, ' + cache.$mobileNavContainer[0].scrollHeight + 'px, 0)' 2587 }); 2588 2589 slate.a11y.trapFocus({ 2590 $container: cache.$sectionHeader, 2591 $elementToFocus: cache.$mobileNavToggle, 2592 namespace: 'navFocus' 2593 }); 2594 2595 cache.$mobileNavToggle
2596 .addClass(classes.mobileNavCloseIcon) 2597 .removeClass(classes.mobileNavOpenIcon) 2598 .attr('aria-expanded', true); 2599 2600 // close on escape 2601 $(window).on('keyup.mobileNav', function(evt) { 2602 if (evt.which === 27) { 2603 closeMobileNav(); 2604 } 2605 }); 2606 } 2607 2608 function closeMobileNav() { 2609 cache.$mobileNavContainer.prepareTransition().removeClass(classes.navOpen); 2610 2611 cache.$mobileNavContainer.css({ 2612 transform: 'translateY(-100%)' 2613 }); 2614 2615 cache.$pageContainer.removeAttr('style'); 2616 2617 slate.a11y.trapFocus({ 2618 $container: $('html'), 2619 $elementToFocus: $('body') 2620 }); 2621 2622 cache.$mobileNavContainer.one( 2623 'TransitionEnd.navToggle webkitTransitionEnd.navToggle transitionend.navToggle oTransitionEnd.navToggle', 2624 function() { 2625 slate.a11y.removeTrapFocus({ 2626 $container: cache.$mobileNav, 2627 namespace: 'navFocus' 2628 }); 2629 } 2630 ); 2631 2632 cache.$mobileNavToggle 2633 .addClass(classes.mobileNavOpenIcon) 2634 .removeClass(classes.mobileNavCloseIcon) 2635 .attr('aria-expanded', false) 2636 .focus(); 2637 2638 $(window).off('keyup.mobileNav'); 2639 2640 scrollTo(0, 0); 2641 } 2642 2643 function toggleSubNav(evt) { 2644 if (isTransitioning) { 2645 return; 2646 } 2647 2648 var $toggleBtn = $(evt.currentTarget);
2649 var isReturn = $toggleBtn.hasClass(classes.return); 2650 isTransitioning = true; 2651 2652 if (isReturn) { 2653 // Close all subnavs by removing active class on buttons 2654 $( 2655 '.' + classes.subNavToggleBtn + '[data-level="' + (menuLevel - 1) + '"]' 2656 ).removeClass(classes.subNavActive); 2657 2658 if ($activeTrigger && $activeTrigger.length) { 2659 $activeTrigger.removeClass(classes.subNavActive); 2660 } 2661 } else { 2662 $toggleBtn.addClass(classes.subNavActive); 2663 } 2664 2665 $activeTrigger = $toggleBtn; 2666 2667 goToSubnav($toggleBtn.data('target')); 2668 } 2669 2670 function goToSubnav(target) { 2671 /*eslint-disable shopify/jquery-dollar-sign-reference */ 2672 2673 var $targetMenu = target 2674 ? $('.mobile-nav__dropdown[data-parent="' + target + '"]') 2675 : cache.$mobileNav; 2676 2677 menuLevel = $targetMenu.data('level') ? $targetMenu.data('level') : 1; 2678 2679 if ($activeSubNav && $activeSubNav.length) { 2680 $activeSubNav.prepareTransition().addClass(classes.subNavClosing); 2681 } 2682 2683 $activeSubNav = $targetMenu; 2684 2685 /*eslint-enable shopify/jquery-dollar-sign-reference */ 2686 2687 var translateMenuHeight = $targetMenu.outerHeight(); 2688 2689 var openNavClass = 2690 menuLevel > 2 ? classes.thirdNavShowing : classes.subNavShowing; 2691 2692 cache.$mobileNavContainer 2693 .css('height', translateMenuHeight) 2694 .removeClass(classes.thirdNavShowing) 2695 .addClass(openNavClass); 2696 2697 if (!target) { 2698 // Show top level nav 2699 cache.$mobileNavContainer 2700 .removeClass(classes.thirdNavShowing) 2701 .removeClass(classes.subNavShowing); 2702 } 2703 2704 /* if going back to first subnav, focus is on whole header */ 2705 var $container = menuLevel === 1 ? cache.$sectionHeader : $targetMenu; 2706 2707 var $menuTitle = $targetMenu.find('[data-menu-title=' + menuLevel + ']'); 2708 var $elementToFocus = $menuTitle ? $menuTitle : $targetMenu; 2709 2710 // Focusing an item in the subnav early forces element into view and breaks the animation. 2711 cache.$mobileNavContainer.one( 2712 'TransitionEnd.subnavToggle webkitTransitionEnd.subnavToggle transitionend.subnavToggle oTransitionEnd.subnavToggle', 2713 function() { 2714 slate.a11y.trapFocus({ 2715 $container: $container, 2716 $elementToFocus: $elementToFocus, 2717 namespace: 'subNavFocus' 2718 }); 2719 2720 cache.$mobileNavContainer.off('.subnavToggle'); 2721 isTransitioning = false; 2722 } 2723 ); 2724 2725 // Match height of subnav 2726 cache.$pageContainer.css({ 2727 transform: 'translateY(' + translateMenuHeight + 'px)' 2728 }); 2729 2730 $activeSubNav.removeClass(classes.subNavClosing); 2731 } 2732 2733 return { 2734 init: init, 2735 closeMobileNav: closeMobileNav 2736 }; 2737})(jQuery); 2738 2739(function() { 2740 var selectors = { 2741 backButton: '.return-link' 2742 }; 2743 2744 var $backButton = $(selectors.backButton); 2745 2746 if (!document.referrer || !$backButton.length || !window.history.length) { 2747 return; 2748 } 2749 2750 $backButton.one('click', function(evt) { 2751 evt.preventDefault(); 2752 2753 var referrerDomain = urlDomain(document.referrer); 2754 var shopDomain = urlDomain(window.location.href); 2755 2756 if (shopDomain === referrerDomain) { 2757 history.back(); 2758 } 2759 2760 return false; 2761 }); 2762 2763 function urlDomain(url) { 2764 var anchor = document.createElement('a'); 2765 anchor.ref = url; 2766 2767 return anchor.hostname; 2768 } 2769})(); 2770 2771theme.Slideshow = (function() { 2772 this.$slideshow = null; 2773 var classes = { 2774 slideshow: 'slideshow', 2775 slickActiveMobile: 'slick-active-mobile', 2776 controlsHover: 'slideshow__controls--hover', 2777 isPaused: 'is-paused' 2778 }; 2779 2780 var selectors = { 2781 section: '.shopify-section', 2782 wrapper: '#SlideshowWrapper-', 2783 slides: '.slideshow__slide', 2784 textWrapperMobile: '.slideshow__text-wrap--mobile', 2785 textContentMobile: '.slideshow__text-content--mobile', 2786 controls: '.slideshow__controls', 2787 pauseButton: '.slideshow__pause', 2788 dots: '.slick-dots', 2789 arrows: '.slideshow__arrows', 2790 arrowsMobile: '.slideshow__arrows--mobile', 2791 arrowLeft: '.slideshow__arrow-left', 2792 arrowRight: '.slideshow__arrow-right' 2793 }; 2794 2795 function slideshow(el, sectionId) { 2796 var $slideshow = (this.$slideshow = $(el)); 2797 this.adaptHeight = this.$slideshow.data('adapt-height'); 2798 this.$wrapper = this.$slideshow.closest(selectors.wrapper + sectionId); 2799 this.$section = this.$wrapper.closest(selectors.section); 2800 this.$controls = this.$wrapper.find(selectors.controls); 2801 this.$arrows = this.$section.find(selectors.arrows); 2802 this.$arrowsMobile = this.$section.find(selectors.arrowsMobile); 2803 this.$pause = this.$controls.find(selectors.pauseButton); 2804 this.$textWrapperMobile = this.$section.find(selectors.textWrapperMobile); 2805 this.autorotate = this.$slideshow.data('autorotate'); 2806 var autoplaySpeed = this.$slideshow.data('speed'); 2807 var loadSlideA11yString = this.$slideshow.data('slide-nav-a11y'); 2808 2809 this.settings = { 2810 accessibility: true,
2811 arrows: false, 2812 dots: true, 2813 fade: true, 2814 draggable: true, 2815 touchThreshold: 20, 2816 autoplay: this.autorotate, 2817 autoplaySpeed: autoplaySpeed, 2818 // eslint-disable-next-line shopify/jquery-dollar-sign-reference 2819 appendDots: this.$arrows, 2820 customPaging: function(slick, index) { 2821 return ( 2822 '<a href="' + 2823 selectors.wrapper + 2824 sectionId + 2825 '" aria-label="' + 2826 loadSlideA11yString.replace('[slide_number]', index + 1) + 2827 '" data-slide-number="' + 2828 index + 2829 '"></a>' 2830 ); 2831 } 2832 }; 2833 2834 this.$slideshow.on('beforeChange', beforeChange.bind(this)); 2835 this.$slideshow.on('init', slideshowA11ySetup.bind(this)); 2836 2837 // Add class to style mobile dots & show the correct text content for the 2838 // first slide on mobile when the slideshow initialises 2839 this.$slideshow.on( 2840 'init', 2841 function() { 2842 this.$mobileDots 2843 .find('li:first-of-type') 2844 .addClass(classes.slickActiveMobile); 2845 this.showMobileText(0); 2846 }.bind(this) 2847 ); 2848 2849 // Stop the autorotate when you scroll past the mobile controls, resume when 2850 // they are scrolled back into view 2851 if (this.autorotate) { 2852 $(document).scroll( 2853 $.debounce( 2854 250, 2855 function() { 2856 if ( 2857 this.$arrowsMobile.offset().top + 2858 this.$arrowsMobile.outerHeight() < 2859 window.pageYOffset 2860 ) { 2861 $slideshow.slick('slickPause'); 2862 }
2862 else if (!this.$pause.hasClass(classes.isPaused)) { 2863 $slideshow.slick('slickPlay'); 2864 } 2865 }.bind(this) 2866 ) 2867 ); 2868 } 2869 2870 if (this.adaptHeight) { 2871 this.setSlideshowHeight(); 2872 $(window).resize($.debounce(50, this.setSlideshowHeight.bind(this))); 2873 } 2874 2875 this.$slideshow.slick(this.settings); 2876 2877 // This can't be called when the slick 'init' event fires due to how slick 2878 // adds a11y features. 2879 slideshowPostInitA11ySetup.bind(this)(); 2880 2881 this.$arrows.find(selectors.arrowLeft).on('click', function() { 2882 $slideshow.slick('slickPrev'); 2883 }); 2884 this.$arrows.find(selectors.arrowRight).on('click', function() { 2885 $slideshow.slick('slickNext'); 2886 }); 2887 2888 this.$pause.on('click', this.togglePause.bind(this)); 2889 } 2890 2891 function slideshowA11ySetup(event, obj) { 2892 var $slider = obj.$slider; 2893 var $list = obj.$list; 2894 this.$dots = this.$section.find(selectors.dots); 2895 this.$mobileDots = this.$dots.eq(1); 2896 2897 // Remove default Slick aria-live attr until slider is focused 2898 $list.removeAttr('aria-live'); 2899 2900 this.$wrapper.on('keyup', keyboardNavigation.bind(this)); 2901 this.$controls.on('keyup', keyboardNavigation.bind(this)); 2902 this.$textWrapperMobile.on('keyup', keyboardNavigation.bind(this)); 2903 2904 // When an element in the slider is focused 2905 // pause slideshow and set aria-live. 2906 this.$wrapper 2907 .on( 2908 'focusin', 2909 function(evt) { 2910 if (!this.$wrapper.has(evt.target).length) { 2911 return; 2912 } 2913 2914 $list.attr('aria-live', 'polite'); 2915 if (this.autorotate) { 2916 $slider.slick('slickPause'); 2917 } 2918 }.bind(this) 2919 ) 2920 .on( 2921 'focusout', 2922 function(evt) { 2923 if (!this.$wrapper.has(evt.target).length) { 2924 return; 2925 } 2926 2927 $list.removeAttr('aria-live'); 2928 if (this.autorotate) { 2929 // Only resume playing if the user hasn't paused using the pause 2930 // button 2931 if (!this.$pause.is('.is-paused')) { 2932 $slider.slick('slickPlay'); 2933 } 2934 } 2935 }.bind(this) 2936 ); 2937 2938 // Add arrow key support when focused 2939 if (this.$dots) { 2940 this.$dots 2941 .find('a') 2942 .each(function() { 2943 var $dot = $(this); 2944 $dot.on('click keyup', function(evt) { 2945 if ( 2946 evt.type === 'keyup' && 2947 evt.which !== slate.utils.keyboardKeys.ENTER 2948 ) 2949 return; 2950 2951 evt.preventDefault(); 2952 2953 var slideNumber = $(evt.target).data('slide-number'); 2954 2955 $slider.attr('tabindex', -1).slick('slickGoTo', slideNumber); 2956 2957 if (evt.type === 'keyup') { 2958 $slider.focus(); 2959 } 2960 }); 2961 }) 2962 .eq(0) 2963 .attr('aria-current', 'true'); 2964 } 2965 2966 this.$controls 2967 .on('focusin', highlightControls.bind(this)) 2968 .on('focusout', unhighlightControls.bind(this)); 2969 } 2970 2971 function slideshowPostInitA11ySetup() { 2972 var $slides = this.$slideshow.find(selectors.slides); 2973 2974 $slides.removeAttr('role').removeAttr('aria-labelledby'); 2975 this.$dots 2976 .removeAttr('role') 2977 .find('li') 2978 .removeAttr('role') 2979 .removeAttr('aria-selected') 2980 .each(function() { 2981 var $dot = $(this); 2982 var ariaControls = $dot.attr('aria-controls'); 2983 $dot 2984 .removeAttr('aria-controls') 2985 .find('a') 2986 .attr('aria-controls', ariaControls); 2987 }); 2988 } 2989 2990 function beforeChange(event, slick, currentSlide, nextSlide) { 2991 var $dotLinks = this.$dots.find('a'); 2992 var $mobileDotLinks = this.$mobileDots.find('li'); 2993 2994 $dotLinks 2995 .removeAttr('aria-current') 2996 .eq(nextSlide) 2997 .attr('aria-current', 'true'); 2998 2999 $mobileDotLinks 3000 .removeClass(classes.slickActiveMobile) 3001 .eq(nextSlide)
3002 .addClass(classes.slickActiveMobile); 3003 this.showMobileText(nextSlide); 3004 } 3005 3006 function keyboardNavigation() { 3007 if (event.keyCode === slate.utils.keyboardKeys.LEFTARROW) { 3008 this.$slideshow.slick('slickPrev'); 3009 } 3010 if (event.keyCode === slate.utils.keyboardKeys.RIGHTARROW) { 3011 this.$slideshow.slick('slickNext'); 3012 } 3013 } 3014 3015 function highlightControls() { 3016 this.$controls.addClass(classes.controlsHover); 3017 } 3018 3019 function unhighlightControls() { 3020 this.$controls.removeClass(classes.controlsHover); 3021 } 3022 3023 slideshow.prototype.togglePause = function() { 3024 var slideshowSelector = getSlideshowId(this.$pause); 3025 if (this.$pause.hasClass(classes.isPaused)) { 3026 this.$pause.removeClass(classes.isPaused).attr('aria-pressed', 'false'); 3027 if (this.autorotate) { 3028 $(slideshowSelector).slick('slickPlay'); 3029 } 3030 } else { 3031 this.$pause.addClass(classes.isPaused).attr('aria-pressed', 'true'); 3032 if (this.autorotate) { 3033 $(slideshowSelector).slick('slickPause'); 3034 } 3035 } 3036 }; 3037 3038 slideshow.prototype.setSlideshowHeight = function() { 3039 var minAspectRatio = this.$slideshow.data('min-aspect-ratio'); 3040 this.$slideshow.height($(document).width() / minAspectRatio); 3041 }; 3042 3043 slideshow.prototype.showMobileText = function(slideIndex) { 3044 var $allTextContent = this.$textWrapperMobile.find( 3045 selectors.textContentMobile 3046 ); 3047 var currentTextContentSelector = 3048 selectors.textContentMobile + '-' + slideIndex; 3049 var $currentTextContent = this.$textWrapperMobile.find( 3050 currentTextContentSelector 3051 ); 3052 if ( 3053 !$currentTextContent.length && 3054 this.$slideshow.find(selectors.slides).length === 1 3055 ) { 3056 this.$textWrapperMobile.hide(); 3057 } else { 3058 this.$textWrapperMobile.show(); 3059 } 3060 $allTextContent.hide(); 3061 $currentTextContent.show(); 3062 }; 3063 3064 function getSlideshowId($el) { 3065 return '#Slideshow-' + $el.data('id'); 3066 } 3067 3068 return slideshow; 3069})(); 3070 3071theme.Video = (function() { 3072 var autoplayCheckComplete = false; 3073 var playOnClickChecked = false; 3074 var playOnClick = false; 3075 var youtubeLoaded = false; 3076 var videos = {}; 3077 var videoPlayers = []; 3078 var videoOptions = { 3079 ratio: 16 / 9, 3080 scrollAnimationDuration: 400, 3081 playerVars: { 3082 // eslint-disable-next-line camelcase 3083 iv_load_policy: 3, 3084 modestbranding: 1, 3085 autoplay: 0, 3086 controls: 0, 3087 wmode: 'opaque', 3088 branding: 0, 3089 autohide: 0, 3090 rel: 0 3091 }, 3092 events: { 3093 onReady: onPlayerReady, 3094 onStateChange: onPlayerChange 3095 } 3096 }; 3097 var classes = { 3098 playing: 'video-is-playing', 3099 paused: 'video-is-paused', 3100 loading: 'video-is-loading', 3101 loaded: 'video-is-loaded', 3102 backgroundVideoWrapper: 'video-background-wrapper', 3103 videoWithImage: 'video--image_with_play', 3104 backgroundVideo: 'video--background', 3105 userPaused: 'is-paused', 3106 supportsAutoplay: 'autoplay', 3107 supportsNoAutoplay: 'no-autoplay', 3108 wrapperMinHeight: 'video-section-wrapper--min-height' 3109 }; 3110 3111 var selectors = { 3112 section: '.video-section', 3113 videoWrapper: '.video-section-wrapper', 3114 playVideoBtn: '.video-control__play', 3115 closeVideoBtn: '.video-control__close-wrapper', 3116 pauseVideoBtn: '.video__pause', 3117 pauseVideoStop: '.video__pause-stop', 3118 pauseVideoResume: '.video__pause-resume', 3119 fallbackText: '.icon__fallback-text' 3120 }; 3121 3122 /** 3123 * Public functions 3124 */ 3125 function init($video) { 3126 if (!$video.length) { 3127 return; 3128 } 3129 3130 videos[$video.attr('id')] = { 3131 id: $video.attr('id'), 3132 videoId: $video.data('id'), 3133 type: $video.data('type'), 3134 status: 3135 $video.data('type') === 'image_with_play' ? 'closed' : 'background', // closed, open, background 3136 $video: $video, 3137 $videoWrapper: $video.closest(selectors.videoWrapper), 3138 $section: $video.closest(selectors.section), 3139 controls: $video.data('type') === 'background' ? 0 : 1 3140 }; 3141 3142 if (!youtubeLoaded) { 3143 // This code loads the IFrame Player API code asynchronously. 3144 var tag = document.createElement('script'); 3145 tag.src = 'https://www.youtube.com/iframe_api'; 3146 var firstScriptTag = document.getElementsByTagName('script')[0]; 3147 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 3148 } 3149 3150 playOnClickCheck(); 3151 } 3152 3153 function customPlayVideo(playerId) { 3154 // Make sure we have carried out the playOnClick check first 3155 if (!playOnClickChecked && !playOnClick) { 3156 return; 3157 } 3158 3159 if (playerId && typeof videoPlayers[playerId].playVideo === 'function') { 3160 privatePlayVideo(playerId); 3161 } 3162 } 3163 3164 function pauseVideo(playerId) { 3165 if ( 3166 videoPlayers[playerId] && 3167 typeof videoPlayers[playerId].pauseVideo === 'function' 3168 ) { 3169 videoPlayers[playerId].pauseVideo(); 3170 } 3171 } 3172 3173 function loadVideos() { 3174 for (var key in videos) { 3175 if (videos.hasOwnProperty(key)) { 3176 createPlayer(key); 3177 } 3178 } 3179 3180 initEvents(); 3181 youtubeLoaded = true; 3182 } 3183 3184 function editorLoadVideo(key) { 3185 if (!youtubeLoaded) { 3186 return; 3187 } 3188 createPlayer(key); 3189 3190 initEvents(); 3191 } 3192 3193 /** 3194 * Private functions 3195 */ 3196 3197 function privatePlayVideo(id, clicked) { 3198 var videoData = videos[id]; 3199 var player = videoPlayers[id]; 3200 var $videoWrapper = videoData.$videoWrapper; 3201 3202 if (playOnClick) { 3203 // playOnClick means we are probably on mobile (no autoplay). 3204 // setAsPlaying will show the iframe, requiring another click 3205 // to play the video. 3206 setAsPlaying(videoData); 3207 } else if (clicked || autoplayCheckComplete) { 3208 // Play if autoplay is available or clicked to play
3209 $videoWrapper.removeClass(classes.loading); 3210 setAsPlaying(videoData); 3211 player.playVideo(); 3212 return; 3213 } else { 3214 player.playVideo(); 3215 } 3216 } 3217 3218 function setAutoplaySupport(supported) { 3219 var supportClass = supported 3220 ? classes.supportsAutoplay 3221 : classes.supportsNoAutoplay; 3222 $(document.documentElement) 3223 .removeClass(classes.supportsAutoplay) 3224 .removeClass(classes.supportsNoAutoplay) 3225 .addClass(supportClass); 3226 3227 if (!supported) { 3228 playOnClick = true; 3229 } 3230 3231 autoplayCheckComplete = true; 3232 } 3233 3234 function playOnClickCheck() { 3235 // Bail early for a few instances: 3236 // - small screen 3237 // - device sniff mobile browser 3238 3239 if (playOnClickChecked) { 3240 return; 3241 } 3242 3243 if (isMobile()) { 3244 playOnClick = true; 3245 } 3246 3247 if (playOnClick) { 3248 // No need to also do the autoplay check 3249 setAutoplaySupport(false); 3250 } 3251 3252 playOnClickChecked = true; 3253 } 3254 3255 // The API will call this function when each video player is ready 3256 function onPlayerReady(evt) { 3257 evt.target.setPlaybackQuality('hd1080'); 3258 var videoData = getVideoOptions(evt); 3259 var videoTitle = evt.target.getVideoData().title; 3260 playOnClickCheck(); 3261 3262 // Prevent tabbing through YouTube player controls until visible 3263 $('#' + videoData.id).attr('tabindex', '-1'); 3264 3265 sizeBackgroundVideos(); 3266 setButtonLabels(videoData.$videoWrapper, videoTitle); 3267 3268 // Customize based on options from the video ID 3269 if (videoData.type === 'background') { 3270 evt.target.mute(); 3271 privatePlayVideo(videoData.id); 3272 } 3273
3274 videoData.$videoWrapper.addClass(classes.loaded); 3275 } 3276 3277 function onPlayerChange(evt) { 3278 var videoData = getVideoOptions(evt); 3279 if ( 3280 videoData.status === 'background' && 3281 !isMobile() && 3282 !autoplayCheckComplete && 3283 (evt.data === YT.PlayerState.PLAYING || 3284 evt.data === YT.PlayerState.BUFFERING) 3285 ) { 3286 setAutoplaySupport(true); 3287 autoplayCheckComplete = true; 3288 videoData.$videoWrapper.removeClass(classes.loading); 3289 } 3290 switch (evt.data) { 3291 case YT.PlayerState.ENDED: 3292 setAsFinished(videoData); 3293 break; 3294 case YT.PlayerState.PAUSED: 3295 // Seeking on a YouTube video also fires a PAUSED state change, 3296 // checking the state after a delay prevents us pausing the video when 3297 // the user is seeking instead of pausing 3298 setTimeout(function() { 3299 if (evt.target.getPlayerState() === YT.PlayerState.PAUSED) { 3300 setAsPaused(videoData); 3301 } 3302 }, 200); 3303 break; 3304 } 3305 } 3306 3307 function setAsFinished(videoData) { 3308 switch (videoData.type) { 3309 case 'background': 3310 videoPlayers[videoData.id].seekTo(0); 3311 break; 3312 case 'image_with_play': 3313 closeVideo(videoData.id); 3314 toggleExpandVideo(videoData.id, false); 3315 break; 3316 } 3317 } 3318 3319 function setAsPlaying(videoData) { 3320 var $videoWrapper = videoData.$videoWrapper; 3321 var $pauseButton = $videoWrapper.find(selectors.pauseVideoBtn); 3322 3323 $videoWrapper.removeClass(classes.loading); 3324
3325 if ($pauseButton.hasClass(classes.userPaused)) { 3326 $pauseButton.removeClass(classes.userPaused); 3327 } 3328 3329 // Do not change element visibility if it is a background video 3330 if (videoData.status === 'background') { 3331 return; 3332 } 3333 3334 $('#' + videoData.id).attr('tabindex', '0'); 3335 3336 if (videoData.type === 'image_with_play') { 3337 $videoWrapper.removeClass(classes.paused).addClass(classes.playing); 3338 } 3339 3340 // Update focus to the close button so we stay within the video wrapper, 3341 // allowing time for the scroll animation 3342 setTimeout(function() { 3343 $videoWrapper.find(selectors.closeVideoBtn).focus(); 3344 }, videoOptions.scrollAnimationDuration); 3345 } 3346 3347 function setAsPaused(videoData) { 3348 var $videoWrapper = videoData.$videoWrapper; 3349 3350 // YT's events fire after our click event. This status flag ensures 3351 // we don't interact with a closed or background video. 3352 if (videoData.type === 'image_with_play') { 3353 if (videoData.status === 'closed') { 3354 $videoWrapper.removeClass(classe
3354s.paused); 3355 } else { 3356 $videoWrapper.addClass(classes.paused); 3357 } 3358 } 3359 3360 $videoWrapper.removeClass(classes.playing); 3361 } 3362 3363 function closeVideo(playerId) { 3364 var videoData = videos[playerId]; 3365 var $videoWrapper = videoData.$videoWrapper; 3366 var classesToRemove = [classes.paused, classes.playing].join(' '); 3367 3368 if (isMobile()) { 3369 $videoWrapper.removeAttr('style'); 3370 } 3371 3372 $('#' + videoData.id).attr('tabindex', '-1'); 3373 3374 videoData.status = 'closed'; 3375 3376 switch (videoData.type) { 3377 case 'image_with_play': 3378 videoPlayers[playerId].stopVideo(); 3379 setAsPaused(videoData); // in case the video is already paused 3380 break; 3381 case 'background': 3382 videoPlayers[playerId].mute(); 3383 setBackgroundVideo(playerId); 3384 break; 3385 } 3386 3387 $videoWrapper.removeClass(classesToRemove); 3388 } 3389 3390 function getVideoOptions(evt) { 3391 var id = evt.target.getIframe().id; 3392 return videos[id]; 3393 } 3394 3395 function toggleExpandVideo(playerId, expand) { 3396 var video = videos[playerId]; 3397 var elementTop = video.$videoWrapper.offset().top; 3398 var $playButton = video.$videoWrapper.find(selectors.playVideoBtn); 3399 var offset = 0; 3400 var newHeight = 0; 3401 3402 if (isMobile()) { 3403 video.$videoWrapper.parent().toggleClass('page-width', !expand); 3404 } 3405 3406 if (expand) { 3407 if (isMobile()) { 3408 newHeight = $(window).width() / videoOptions.ratio; 3409 } else { 3410 newHeight = video.$videoWrapper.width() / videoOptions.ratio; 3411 } 3412 offset = ($(window).height() - newHeight) / 2; 3413 3414 video.$videoWrapper 3415 .removeClass(classes.wrapperMinHeight) 3416 .animate({ height: newHeight }, 600); 3417 3418 // Animate doesn't work in mobile editor, so we don't use it 3419 if (!(isMobile() && Shopify.designMode)) { 3420 $('html, body').animate( 3421 { 3422 scrollTop: elementTop - offset 3423 }, 3424 videoOptions.scrollAnimationDuration 3425 ); 3426 } 3427 } else { 3428 if (isMobile()) { 3429 newHeight = video.$videoWrapper.data('mobile-height'); 3430 } else { 3431 newHeight = video.$videoWrapper.data('desktop-height'); 3432 } 3433 3434 video.$videoWrapper 3435 .height(video.$videoWrapper.width() / videoOptions.ratio) 3436 .animate({ height: newHeight }, 600); 3437 setTimeout(function() {
3438 video.$videoWrapper.addClass(classes.wrapperMinHeight); 3439 }, 600); 3440 $playButton.focus(); 3441 } 3442 } 3443 3444 function togglePause(playerId) { 3445 var $pauseButton = videos[playerId].$videoWrapper.find( 3446 selectors.pauseVideoBtn 3447 ); 3448 var paused = $pauseButton.hasClass(classes.userPaused); 3449 if (paused) { 3450 $pauseButton.removeClass(classes.userPaused); 3451 customPlayVideo(playerId); 3452 } else { 3453 $pauseButton.addClass(classes.userPaused); 3454 pauseVideo(playerId); 3455 } 3456 $pauseButton.attr('aria-pressed', !paused); 3457 } 3458 3459 function startVideoOnClick(playerId) { 3460 var video = videos[playerId]; 3461 3462 // add loading class to wrapper 3463 video.$videoWrapper.addClass(classes.loading); 3464 3465 // Explicity set the video wrapper height (needed for height transition) 3466 video.$videoWrapper.attr( 3467 'style', 3468 'height: ' + video.$videoWrapper.height() + 'px' 3469 ); 3470 3471 video.status = 'open'; 3472 3473 switch (video.type) { 3474 case 'image_with_play': 3475 privatePlayVideo(playerId, true); 3476 break; 3477 case 'background': 3478 unsetBackgroundVideo(playerId, video); 3479 videoPlayers[playerId].unMute(); 3480 privatePlayVideo(playerId, true); 3481 break; 3482 } 3483 3484 toggleExpandVideo(playerId, true); 3485 3486 // esc to close video player 3487 $(document).on('keydown.videoPlayer', function(evt) { 3488 var playerId = $(document.activeElement).data('controls'); 3489 if (evt.keyCode !== slate.utils.keyboardKeys.ESCAPE || !playerId) { 3490 return; 3491 } 3492 3493 closeVideo(playerId); 3494 toggleExpandVideo(playerId, false); 3495 }); 3496 } 3497 3498 function sizeBackgroundVideos() { 3499 $('.' + classes.backgroundVideo).each(function(index, el) { 3500 sizeBackgroundVideo($(el)); 3501 }); 3502 } 3503 3504 function sizeBackgroundVideo($videoPlayer) { 3505 if (!youtubeLoaded) { 3506 return; 3507 } 3508 3509 if (isMobile()) { 3510 $videoPlayer.removeAttr('style'); 3511 } else { 3512 var $videoWrapper = $videoPlayer.closest(selectors.videoWrapper); 3513 var videoWidth = $videoWrapper.width(); 3514 var playerWidth = $videoPlayer.width(); 3515 var desktopHeight = $videoWrapper.data('desktop-height'); 3516 3517 // when screen aspect ratio differs from video, video must center and underlay one dimension 3518 if (videoWidth / videoOptions.ratio < desktopHeight) { 3519 playerWidth = Math.ceil(desktopHeight * videoOptions.ratio); // get new player width 3520 $videoPlayer 3521 .width(playerWidth) 3522 .height(desktopHeight) 3523 .css({ 3524 left: (videoWidth - playerWidth) / 2, 3525 top: 0 3526 }); // player width is greater, offset left; reset top 3527 } else { 3528 // new video width < window width (gap to right) 3529 desktopHeight = Math.ceil(videoWidth / videoOptions.ratio); // get new player height 3530 $videoPlayer 3531 .width(videoWidth) 3532 .height(desktopHeight) 3533 .css({ 3534 left: 0, 3535 top: (desktopHeight - desktopHeight) / 2 3536 }); // player height is greater, offset top; reset left 3537 } 3538 3539 $videoPlayer.prepareTransition();
3540 $videoWrapper.addClass(classes.loaded); 3541 } 3542 } 3543 3544 function unsetBackgroundVideo(playerId) { 3545 // Switch the background video to a chrome-only player once played 3546 $('#' + playerId) 3547 .removeClass(classes.backgroundVideo) 3548 .addClass(classes.videoWithImage); 3549 3550 setTimeout(function() { 3551 $('#' + playerId).removeAttr('style'); 3552 }, 600); 3553 3554 videos[playerId].$videoWrapper 3555 .removeClass(classes.backgroundVideoWrapper) 3556 .addClass(classes.playing); 3557 3558 videos[playerId].status = 'open'; 3559 } 3560 3561 function setBackgroundVideo(playerId) { 3562 $('#' + playerId) 3563 .removeClass(classes.videoWithImage) 3564 .addClass(classes.backgroundVideo); 3565 3566 videos[playerId].$videoWrapper.addClass(classes.backgroundVideoWrapper); 3567 3568 videos[playerId].status = 'background'; 3569 sizeBackgroundVideo($('#' + playerId)); 3570 } 3571 3572 function isMobile() { 3573 return $(window).width() < 750 || window.mobileCheck(); 3574 } 3575 3576 function initEvents() { 3577 $(document).on('click.videoPlayer', selectors.playVideoBtn, function(evt) { 3578 var playerId = $(evt.currentTarget).data('controls'); 3579 3580 startVideoOnClick(playerId); 3581 }); 3582 3583 $(document).on('click.videoPlayer', selectors.closeVideoBtn, function(evt) { 3584 var playerId = $(evt.currentTarget).data('controls'); 3585 3586 $(evt.currentTarget).blur(); 3587 closeVideo(playerId); 3588 toggleExpandVideo(playerId, false); 3589 }); 3590 3591 $(document).on('click.videoPlayer', selectors.pauseVideoBtn, function(evt) { 3592 var playerId = $(evt.currentTarget).data('controls'); 3593 togglePause(playerId); 3594 }); 3595 3596 // Listen to resize to keep a background-size:cover-like layout 3597 $(window).on( 3598 'resize.videoPlayer', 3599 $.debounce(200, function() { 3600 if (!youtubeLoaded) return; 3601 var key; 3602 var fullscreen = window.innerHeight === screen.height; 3603 3604 sizeBackgroundVideos(); 3605 3606 if (isMobile()) { 3607 for (key in videos) { 3608 if (videos.hasOwnProperty(key)) {
3609 if (videos[key].$videoWrapper.hasClass(classes.playing)) { 3610 if (!fullscreen) { 3611 pauseVideo(key); 3612 setAsPaused(videos[key]); 3613 } 3614 } 3615 videos[key].$videoWrapper.height( 3616 $(document).width() / videoOptions.ratio 3617 ); 3618 } 3619 } 3620 setAutoplaySupport(false); 3621 } else { 3622 setAutoplaySupport(true); 3623 for (key in videos) { 3624 if ( 3625 videos[key].$videoWrapper.find('.' + classes.videoWithImage) 3626 .length 3627 ) { 3628 continue; 3629 } 3630 videoPlayers[key].playVideo(); 3631 setAsPlaying(videos[key]); 3632 } 3633 } 3634 }) 3635 ); 3636 3637 $(window).on( 3638 'scroll.videoPlayer', 3639 $.debounce(50, function() { 3640 if (!youtubeLoaded) return; 3641 3642 for (var key in videos) { 3643 if (videos.hasOwnProperty(key)) { 3644 var $videoWrapper = videos[key].$videoWrapper; 3645 3646 // Close the video if more than 75% of it is scrolled out of view 3647 if (
3648 $videoWrapper.hasClass(classes.playing) && 3649 ($videoWrapper.offset().top + $videoWrapper.height() * 0.75 < 3650 $(window).scrollTop() || 3651 $videoWrapper.offset().top + $videoWrapper.height() * 0.25 > 3652 $(window).scrollTop() + $(window).height()) 3653 ) { 3654 closeVideo(key); 3655 toggleExpandVideo(key, false); 3656 } 3657 } 3658 } 3659 }) 3660 ); 3661 } 3662 3663 function createPlayer(key) { 3664 var args = $.extend({}, videoOptions, videos[key]); 3665 args.playerVars.controls = args.controls; 3666 videoPlayers[key] = new YT.Player(key, args); 3667 } 3668 3669 function removeEvents() { 3670 $(document).off('.videoPlayer'); 3671 $(window).off('.videoPlayer'); 3672 } 3673 3674 function setButtonLabels($videoWrapper, title) { 3675 var $playButtons = $videoWrapper.find(selectors.playVideoBtn); 3676 var $closeButton = $videoWrapper.find(selectors.closeVideoBtn); 3677 var $pauseButton = $videoWrapper.find(selectors.pauseVideoBtn); 3678 var $closeButtonText = $closeButton.find(selectors.fallbackText); 3679 var $pauseButtonStopText = $pauseButton 3680 .find(selectors.pauseVideoStop) 3681 .find(selectors.fallbackText); 3682 var $pauseButtonResumeText = $pauseButton 3683 .find(selectors.pauseVideoResume) 3684 .find(selectors.fallbackText); 3685 3686 // Insert the video title retrieved from YouTube into the instructional text 3687 // for each button 3688 $playButtons.each(function() { 3689 var $playButton = $(this); 3690 var $playButtonText = $playButton.find(selectors.fallbackText); 3691 3692 $playButtonText.text( 3693 $playButtonText.text().replace('[video_title]', title) 3694 ); 3695 }); 3696 $closeButtonText.text( 3697 $closeButtonText.text().replace('[video_title]', title) 3698 ); 3699 $pauseButtonStopText.text( 3700 $pauseButtonStopText.text().replace('[video_title]', title) 3701 ); 3702 $pauseButtonResumeText.text( 3703 $pauseButtonResumeText.text().replace('[video_title]', title) 3704 ); 3705 } 3706 3707 return { 3708 init: init, 3709 editorLoadVideo: editorLoadVideo, 3710 loadVideos: loadVideos, 3711 playVideo: customPlayVideo, 3712 pauseVideo: pauseVideo, 3713 removeEvents: removeEvents 3714 }; 3715})(); 3716 3717theme.ProductVideo = (function() { 3718 var videos = {}; 3719 3720 var hosts = { 3721 html5: 'html5', 3722 youtube: 'youtube' 3723 }; 3724 3725 var selectors = { 3726 productMediaWrapper: '[data-product-single-media-wrapper]' 3727 }; 3728 3729 var attributes = { 3730 enableVideoLooping: 'enable-video-looping', 3731 videoId: 'video-id' 3732 }; 3733 3734 function init(videoContainer, sectionId) { 3735 if (!videoContainer.length) { 3736 return; 3737 } 3738 3739 var videoElement = videoContainer.find('iframe, video')[0]; 3740 var mediaId = videoContainer.data('mediaId'); 3741 3742 if (!videoElement) { 3743 return; 3744 } 3745 3746 videos[mediaId] = { 3747 mediaId: mediaId, 3748 sectionId: sectionId, 3749 host: hostFromVideoElement(videoElement), 3750 container: videoContainer, 3751 element: videoElement, 3752 ready: function() { 3753 createPlayer(this); 3754 } 3755 }; 3756 3757 var video = videos[mediaId]; 3758 3759 switch (video.host) { 3760 case hosts.html5: 3761 window.Shopify.loadFeatures([ 3762 { 3763 name: 'video-ui', 3764 version: '1.0', 3765 onLoad: setupPlyrVideos 3766 } 3767 ]); 3768 theme.LibraryLoader.load('plyrShopifyStyles'); 3769 break; 3770 case hosts.youtube: 3771 theme.LibraryLoader.load('youtubeSdk', setupYouTubeVideos); 3772 break; 3773 } 3774 } 3775 3776 function setupPlyrVideos(errors) { 3777 if (errors) { 3778 fallbackToNativeVideo(); 3779 return; 3780 } 3781 3782 loadVideos(hosts.html5); 3783 } 3784 3785 function setupYouTubeVideos() { 3786 if (!window.YT.Player) return; 3787 3788 loadVideos(hosts.youtube); 3789 } 3790 3791 function createPlayer(video) { 3792 if (video.player) { 3793 return; 3794 } 3795 3796 var productMediaWrapper = video.container.closest( 3797 selectors.productMediaWrapper 3798 ); 3799 var enableLooping = productMediaWrapper.data(attributes.enableVideoLooping); 3800 3801 switch (video.host) { 3802 case hosts.html5: 3803 // eslint-disable-next-line no-undef 3804 video.player = new Shopify.Plyr(video.element, { 3805 loop: { active: enableLooping } 3806 }); 3807 break; 3808 case hosts.youtube: 3809 var videoId = productMediaWrapper.data(attributes.videoId); 3810 3811 video.player = new YT.Player(video.element, { 3812 videoId: videoId, 3813 events: { 3814 onStateChange: function(event) { 3815 if (event.data === 0 && enableLooping) event.target.seekTo(0); 3816 } 3817 } 3818 }); 3819 break; 3820 } 3821 3822 productMediaWrapper.on('mediaHidden xrLaunch', function() { 3823 if (!video.player) return; 3824 3825 if (video.host === hosts.html5) { 3826 video.player.pause(); 3827 } 3828 3829 if (video.host === hosts.youtube && video.player.pauseVideo) { 3830 video.player.pauseVideo(); 3831 } 3832 }); 3833 3834 productMediaWrapper.on('mediaVisible', function() { 3835 if (theme.Helpers.isTouch()) return; 3836 if (!video.player) return; 3837 3838 if (video.host === hosts.html5) { 3839 video.player.play(); 3840 } 3841 3842 if (video.host === hosts.youtube && video.player.playVideo) { 3843 video.player.playVideo(); 3844 } 3845 }); 3846 } 3847 3848 function hostFromVideoElement(video) { 3849 if (video.tagName === 'VIDEO') { 3850 return hosts.html5; 3851 } 3852 3853 if (video.tagName === 'IFRAME') { 3854 if ( 3855 /^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test( 3856 video.src 3857 ) 3858 ) { 3859 return hosts.youtube; 3860 } 3861 } 3862 return null; 3863 } 3864 3865 function loadVideos(host) { 3866 for (var key in videos) { 3867 if (videos.hasOwnProperty(key)) { 3868 var video = videos[key]; 3869 if (video.host === host) { 3870 video.ready(); 3871 } 3872 } 3873 } 3874 } 3875 3876 function fallbackToNativeVideo() { 3877 for (var key in videos) { 3878 if (videos.hasOwnProperty(key)) { 3879 var video = videos[key]; 3880 3881 if (video.nativeVideo) continue; 3882 3883 if (video.host === hosts.html5) { 3884 video.element.setAttribute('controls', 'controls'); 3885 video.nativeVideo = true; 3886 } 3887 } 3888 } 3889 } 3890 3891 function removeSectionVideos(sectionId) { 3892 for (var key in videos) { 3893 if (videos.hasOwnProperty(key)) { 3894 var video = videos[key]; 3895 3896 if (video.sectionId === sectionId) { 3897 if (video.player) video.player.destroy(); 3898 delete videos[key]; 3899 } 3900 } 3901 } 3902 } 3903 3904 return { 3905 init: init, 3906 hosts: hosts, 3907 loadVideos: loadVideos, 3908 removeSectionVideos: removeSectionVideos 3909 }; 3910})(); 3911 3912theme.ProductModel = (function() { 3913 var modelJsonSections = {}; 3914 var models = {}; 3915 var xrButtons = {}; 3916 3917 var selectors = { 3918 mediaGroup: '[data-product-single-media-group]', 3919 xrButton: '[data-shopify-xr]' 3920 }; 3921 3922 function init(modelViewerContainers, sectionId) { 3923 modelJsonSections[sectionId] = { 3924 loaded: false 3925 }; 3926 3927 modelViewerContainers.each(function(index) { 3928 var $modelViewerContainer = $(this); 3929 var mediaId = $modelViewerContainer.data('media-id'); 3930 var $modelViewerElement = $( 3931 $modelViewerContainer.find('model-viewer')[0] 3932 ); 3933 var modelId = $modelViewerElement.data('model-id'); 3934 3935 if (index === 0) { 3936 var $xrButton = $modelViewerContainer 3937 .closest(selectors.mediaGroup) 3938 .find(selectors.xrButton); 3939 xrButtons[sectionId] = { 3940 $element: $xrButton, 3941 defaultId: modelId 3942 }; 3943 } 3944 3945 models[mediaId] = { 3946 modelId: modelId, 3947 sectionId: sectionId, 3948 $container: $modelViewerContainer, 3949 $element: $modelViewerElement 3950 }; 3951 }); 3952 3953 window.Shopify.loadFeatures([ 3954 { 3955 name: 'shopify-xr', 3956 version: '1.0', 3957 onLoad: setupShopifyXr 3958 }, 3959 { 3960 name: 'model-viewer-ui', 3961 version: '1.0', 3962 onLoad: setupModelViewerUi 3963 } 3964 ]); 3965 theme.LibraryLoader.load('modelViewerUiStyles'); 3966 } 3967 3968 function setupShopifyXr(errors) { 3969 if (errors) return; 3970 3971 if (!window.ShopifyXR) { 3972 document.addEventListener('shopify_xr_initialized', function() { 3973 setupShopifyXr(); 3974 }); 3975 return; 3976 } 3977 3978 for (var sectionId in modelJsonSections) { 3979 if (modelJsonSections.hasOwnProperty(sectionId)) { 3980 var modelSection = modelJsonSections[sectionId]; 3981 3982 if (modelSection.loaded) continue; 3983 var $modelJson = $('#ModelJson-' + sectionId); 3984 3985 window.ShopifyXR.addModels(JSON.parse($modelJson.html())); 3986 modelSection.loaded = true; 3987 } 3988 } 3989 window.ShopifyXR.setupXRElements(); 3990 } 3991 3992 function setupModelViewerUi(errors) { 3993 if (errors) return; 3994 3995 for (var key in models) { 3996 if (models.hasOwnProperty(key)) { 3997 var model = models[key]; 3998 if (!model.modelViewerUi) { 3999 model.modelViewerUi = new Shopify.ModelViewerUI(model.$element); 4000 } 4001 setupModelViewerListeners(model); 4002 } 4003 } 4004 } 4005 4006 function setupModelViewerListeners(model) { 4007 var xrButton = xrButtons[model.sectionId]; 4008 4009 model.$container.on('mediaVisible', function() { 4010 xrButton.$element.attr('data-shopify-model3d-id', model.modelId); 4011 if (theme.Helpers.isTouch()) return; 4012 model.modelViewerUi.play(); 4013 }); 4014 4015 model.$container 4016 .on('mediaHidden', function() { 4017 xrButton.$element.attr('data-shopify-model3d-id', xrButton.defaultId); 4018 model.modelViewerUi.pause(); 4019 }) 4020 .on('xrLaunch', function() { 4021 model.modelViewerUi.pause(); 4022 }); 4023 } 4024 4025 function removeSectionModels(sectionId) { 4026 for (var key in models) { 4027 if (models.hasOwnProperty(key)) { 4028 var model = models[key]; 4029 if (model.sectionId === sectionId) { 4030 models[key].modelViewerUi.destroy(); 4031 delete models[key]; 4032 } 4033 } 4034 } 4035 delete modelJsonSections[sectionId]; 4036 } 4037 4038 return { 4039 init: init, 4040 removeSectionModels: removeSectionModels 4041 }; 4042})(); 4043 4044window.theme = window.theme || {}; 4045 4046theme.FormStatus = (function() { 4047 var selectors = { 4048 statusMessage: '[data-form-status]' 4049 }; 4050
4051 function init() { 4052 this.$statusMessage = $(selectors.statusMessage); 4053 4054 if (!this.$statusMessage) return; 4055 4056 this.$statusMessage.attr('tabindex', -1).focus(); 4057 4058 this.$statusMessage.on('blur', handleBlur.bind(this)); 4059 } 4060 4061 function handleBlur() { 4062 this.$statusMessage.removeAttr('tabindex'); 4063 } 4064 4065 return { 4066 init: init 4067 }; 4068})(); 4069 4070theme.Hero = (function() { 4071 var classes = { 4072 indexSectionFlush: 'index-section--flush' 4073 }; 4074 4075 var selectors = { 4076 heroFixedWidthContent: '.hero-fixed-width__content', 4077 heroFixedWidthImage: '.hero-fixed-width__image' 4078 }; 4079 4080 function hero(el, sectionId) { 4081 this.$hero = $(el); 4082 this.layout = this.$hero.data('layout'); 4083 var $parentSection = $('#shopify-section-' + sectionId); 4084 var $heroContent = $parentSection.find(selectors.heroFixedWidthContent); 4085 var $heroImage = $parentSection.find(selectors.heroFixedWidthImage); 4086 4087 if (this.layout !== 'fixed_width') { 4088 return; 4089 } 4090 4091 $parentSection.removeClass(classes.indexSectionFlush); 4092 heroFixedHeight(); 4093 $(window).resize( 4094 $.debounce(50, function() { 4095 heroFixedHeight(); 4096 }) 4097 ); 4098 4099 function heroFixedHeight() { 4100 var contentHeight = $heroContent.height() + 50; 4101 var imageHeight = $heroImage.height(); 4102 4103 if (contentHeight > imageHeight) { 4104 $heroImage.css('min-height', contentHeight); 4105 } 4106 } 4107 } 4108 4109 return hero; 4110})(); 4111 4112// prettier-ignore 4113window.theme = window.theme || {}; 4114 4115theme.SearchResultsTemplate = (function() { 4116 function renderResults(products, isLoading, searchQuery) { 4117 return [ 4118 '<div class="predictive-search">', 4119 renderHeader(products, isLoading), 4120 renderProducts(products, searchQuery), 4121 '</div>' 4122 ].join(''); 4123 } 4124 4125 function renderHeader(products, isLoading) { 4126 if (products.length === 0) { 4127 return ''; 4128 } 4129 4130 return [ 4131 '<div class="predictive-search-title">', 4132 '<h3 id="predictive-search" class="predictive-search-title__content">' + 4133 theme.strings.products + 4134 '</h3>', 4135 '<span class="predictive-search-title__loading-spinner">' + 4136 (isLoading 4137 ? '<span class= "icon-predictive-search-spinner" ></span >' 4138 : '') + 4139 '</span>', 4140 '</div>' 4141 ].join(''); 4142 } 4143 4144 function loadingState() { 4145 return [ 4146 '<div class="predictive-search">', 4147 '<div class="predictive-search-loading">', 4148 '<span class="visually-hidden">' + theme.strings.loading + '</span>', 4149 '<span class="predictive-search-loading__icon">', 4150 '<span class="icon-predictive-search-spinner"></span>', 4151 '</span>', 4152 '</div>', 4153 '</div>' 4154 ].join(''); 4155 } 4156 4157 function renderViewAll(searchQuery) { 4158 return [ 4159 '<button type="submit" class="predictive-search-view-all__button" tabindex="-1">', 4160 theme.strings.searchFor + 4161 '<span class="predictive-search-view-all__query"> “' + 4162 _htmlEscape(searchQuery) + 4163 '”</span>', 4164 '</button>' 4165 ].join(''); 4166 } 4167 4168 function renderProducts(products, searchQuery) { 4169 var resultsCount = products.length; 4170 4171 return [ 4172 '<ul id="predictive-search-results" class="predictive-search__list" role="listbox" aria-labelledby="predictive-search">', 4173 products 4174 .map(function(product, index) { 4175 return renderProduct(normalizeProduct(product), index, resultsCount); 4176 }) 4177 .join(''), 4178 '<li id="search-all" class="predictive-search-view-all" role="option" data-search-result>' + 4179 renderViewAll(searchQuery) + 4180 '</li>', 4181 '</ul>' 4182 ].join(''); 4183 } 4184 4185 function renderProduct(product, index, resultsCount) { 4186 return [ 4187 '<li id="search-result-' + 4188 index + 4189 '" class="predictive-search-item" role="option" data-search-result>', 4190 '<a class="predictive-search-item__link" href="' + 4191 product.url + 4192 '" tabindex="-1">', 4193 '<div class="predictive-search__column predictive-search__column--image" data-image-with-placeholder-wrapper>', 4194 renderProductImage(product), 4195 '</div>', 4196 '<div class="predictive-search__column predictive-search__column--content ' + 4197 (getDetailsCount() ? '' : 'predictive-search__column--center') + 4198 '">', 4199 '<span class="predictive-search-item__title">', 4200 '<span class="predictive-search-item__title-text">' + 4201 product.title + 4202 '</span>', 4203 '</span>' + (getDetailsCount() ? renderProductDetails(product) : ''), 4204 '<span class="visually-hidden">, </span>', 4205 '<span class="visually-hidden">' + 4206 getNumberOfResultsString(index + 1, resultsCount) + 4207 '</span>', 4208 '</div>', 4209 '</a>', 4210 '</li>' 4211 ].join(''); 4212 } 4213 4214 function renderProductImage(product) { 4215 if (product.image === null) { 4216 return ''; 4217 } 4218 4219 return [ 4220 '<div class="placeholder-background placeholder-background--animation" data-image-placeholder aria-hidden="true"></div>', 4221 '<img class="predictive-search-item__image lazyload" src="' + 4222 product.image.url + 4223 '" data-src="' + 4224 product.image.url + 4225 '" data-image alt="' + 4226 product.image.alt + 4227 '" />' 4228 ].join(''); 4229 } 4230 4231 function renderProductDetails(product) { 4232 return [ 4233 '<dl class="predictive-search-item__details price' + 4234 (product.isOnSale ? ' price--on-sale' : '') + 4235 (!product.available ? ' price--sold-out' : '') + 4236 (!product.isPriceVaries && product.isCompareVaries 4237 ? ' price--compare-price-hidden' 4238 : '') + 4239 '">', 4240 '<div class="predictive-search-item__detail">', 4241 renderVendor(product), 4242 '</div>', 4243 '<div class="predictive-search-item__detail predictive-search-item__detail--inline">' + 4244 renderProductPrice(product), 4245 '</div>', 4246 '</dl>' 4247 ].join(''); 4248 } 4249 function renderProductPrice(product) { 4250 if (!theme.settings.predictiveSearchShowPrice) { 4251 return ''; 4252 } 4253 4254 var accessibilityAnnounceComma = '<span class="visually-hidden">, </span>'; 4255 4256 var priceMarkup = 4257 '<div class="price__regular">' + renderPrice(product) + '</div>'; 4258 4259 var salePriceMarkup = 4260 '<div class="price__sale">' + renderSalePrice(product) + '</div>'; 4261 4262 return ( 4263 accessibilityAnnounceComma + 4264 '<div class="price__pricing-group">' + 4265 (product.isOnSale ? salePriceMarkup : priceMarkup) + 4266 '</div>' 4267 ); 4268 } 4269
4270 function renderSalePrice(product) { 4271 return [ 4272 '<dt>', 4273 '<span class="visually-hidden">' + theme.strings.salePrice + '</span>', 4274 '</dt>', 4275 '<dd>', 4276 '<span class="predictive-search-item__price predictive-search-item__price--sale">' + 4277 (product.isPriceVaries 4278 ? theme.strings.fromLowestPrice.replace('[price]', product.price) 4279 : product.price) + 4280 '</span>', 4281 '</dd>', 4282 '<div class="price__compare">' + renderCompareAtPrice(product) + '</div>' 4283 ].join(''); 4284 } 4285 4286 function renderCompareAtPrice(product) { 4287 return [ 4288 '<dt>', 4289 '<span class="visually-hidden">' + 4290 theme.strings.regularPrice + 4291 '</span> ', 4292 '</dt>', 4293 '<dd>', 4294 '<span class="predictive-search-item__price predictive-search-item__price--compare">' + 4295 product.compareAtPrice + 4296 '</span>', 4297 '</dd>' 4298 ].join(''); 4299 } 4300 4301 function renderPrice(product) { 4302 return [ 4303 '<dt>', 4304 '<span class="visually-hidden">' + theme.strings.regularPrice + '</span>', 4305 '</dt>', 4306 '<dd>', 4307 '<span class="predictive-search-item__price">' + 4308 (product.isPriceVaries 4309 ? theme.strings.fromLowestPrice.replace('[price]', product.price) 4310 : product.price) + 4311 '</span>', 4312 '</dd>' 4313 ].join(''); 4314 } 4315 4316 function renderVendor(product) { 4317 if (!theme.settings.predictiveSearchShowVendor || product.vendor === '') { 4318 return ''; 4319 } 4320 4321 return [ 4322 '<dt>', 4323 '<span class="visually-hidden">' + theme.strings.vendor + '</span>', 4324 '</dt>', 4325 '<dd class="predictive-search-item__vendor">' + product.vendor + '</dd>' 4326 ].join(''); 4327 } 4328 4329 function normalizeProduct(product) { 4330 var productOrVariant = 4331 product.variants.length > 0 ? product.variants[0] : product; 4332 4333 return { 4334 url: productOrVariant.url, 4335 image: getProductImage(product), 4336 title: product.title, 4337 vendor: product.vendor || '', 4338 price: theme.Currency.formatMoney(product.price_min, theme.moneyFormat), 4339 compareAtPrice: theme.Currency.formatMoney( 4340 product.compare_at_price_min, 4341 theme.moneyFormat 4342 ), 4343 available: product.available, 4344 isOnSale: isOnSale(product), 4345 isPriceVaries: isPriceVaries(product), 4346 isCompareVaries: isCompareVaries(product) 4347 }; 4348 } 4349 4350 function getProductImage(product) { 4351 var image; 4352 var featuredImage; 4353 4354 if (product.variants.length > 0 && product.variants[0].image !== null) { 4355 featuredImage = product.variants[0].featured_image; 4356 } else if (product.image) { 4357 featuredImage = product.featured_image; 4358 } else { 4359 image = null; 4360 } 4361 4362 if (image !== null) { 4363 image = { 4364 url: theme.Images.getSizedImageUrl(featuredImage.url, '100x'), 4365 alt: featuredImage.alt 4366 }; 4367 } 4368 4369 return image; 4370 } 4371 4372 function isOnSale(product) { 4373 return ( 4374 product.compare_at_price_min !== null && 4375 parseInt(product.compare_at_price_min, 10) > 4376 parseInt(product.price_min, 10) 4377 ); 4378 } 4379 4380 function isPriceVaries(product) { 4381 return product.price_max !== product.price_min; 4382 } 4383 4384 function isCompareVaries(product) { 4385 return product.compare_at_price_max !== product.compare_at_price_min; 4386 } 4387 4388 // Returns the number of optional product details to be shown, 4389 // values of the detailsList need to be boolean. 4390 function getDetailsCount() { 4391 var detailsList = [ 4392 theme.settings.predictiveSearchShowPrice, 4393 theme.settings.predictiveSearchShowVendor 4394 ]; 4395 4396 var detailsCount = detailsList.reduce(function(acc, detail) { 4397 return acc + (detail ? 1 : 0); 4398 }, 0); 4399 4400 return detailsCount; 4401 } 4402 4403 function getNumberOfResultsString(resultNumber, resultsCount) { 4404 return theme.strings.number_of_results 4405 .replace('[result_number]', resultNumber) 4406 .replace('[results_count]', resultsCount); 4407 } 4408 4409 function _htmlEscape(input) { 4410 return input 4411 .replace(/&/g, '&') 4412 .replace(/</g, '<') 4413 .replace(/>/g, '>') 4414 .replace(/"/g, '"') 4415 .replace(/'/g, '''); 4416 } 4417 4418 return function(data) { 4419 var products = data.products || []; 4420 var isLoading = data.isLoading; 4421 var searchQuery = data.searchQuery || ''; 4422 4423 if (isLoading && products.length === 0) { 4424 return loadingState(); 4425 } 4426 4427 return renderResults(products, isLoading, searchQuery); 4428 }; 4429})(); 4430 4431window.theme = window.theme || {}; 4432 4433(function() { 4434 // (a11y) This function will be used by the Predictive Search Component 4435 // to announce the number of search results 4436 function numberOfResultsTemplateFct(data) { 4437 if (data.products.length === 1) { 4438 return theme.strings.one_result_found; 4439 } else { 4440 return theme.strings.number_of_results_found.replace( 4441 '[results_count]', 4442 data.products.length 4443 ); 4444 } 4445 } 4446 4447 // (a11y) This function will be used by the Predictive Search Component 4448 // to announce that it's loading results 4449 function loadingResultsMessageTemplateFct() { 4450 return theme.strings.loading; 4451 } 4452 4453 function isPredictiveSearchSupported() { 4454 var shopifyFeatures = JSON.parse($('#shopify-features').text()); 4455 4456 return shopifyFeatures.predictiveSearch; 4457 } 4458 4459 function isPredictiveSearchEnabled() { 4460 return window.theme.settings.predictiveSearchEnabled; 4461 } 4462 4463 function canInitializePredictiveSearch() { 4464 return isPredictiveSearchSupported() && isPredictiveSearchEnabled(); 4465 } 4466 4467 // listen for search submits and validate query 4468 function validateSearchHandler(searchEl, submitEl) { 4469 submitEl.addEventListener( 4470 'click', 4471 validateSearchInput.bind(this, searchEl) 4472 ); 4473 } 4474
4475 // if there is nothing in the search field, prevent submit 4476 function validateSearchInput(searchEl, evt) { 4477 var isInputValueEmpty = searchEl.value.trim().length === 0; 4478 if (!isInputValueEmpty) { 4479 return; 4480 } 4481 4482 if (typeof evt !== 'undefined') { 4483 evt.preventDefault(); 4484 } 4485 4486 searchEl.focus(); 4487 } 4488 4489 window.theme.SearchPage = (function() { 4490 var selectors = { 4491 searchReset: '[data-search-page-predictive-search-clear]', 4492 searchInput: '[data-search-page-predictive-search-input]', 4493 searchSubmit: '[data-search-page-predictive-search-submit]', 4494 searchResults: '[data-predictive-search-mount="default"]' 4495 }; 4496 4497 var componentInstance; 4498 var searchInput = document.querySelector(selectors.searchInput); 4499 var searchSubmit = document.querySelector(selectors.searchSubmit); 4500 4501 function init(config) { 4502 componentInstance = new window.Shopify.theme.PredictiveSearchComponent({ 4503 selectors: { 4504 input: selectors.searchInput, 4505 reset: selectors.searchReset, 4506 result: selectors.searchResults 4507 }, 4508 resultTemplateFct: window.theme.SearchResultsTemplate, 4509 numberOfResultsTemplateFct: numberOfResultsTemplateFct, 4510 loadingResultsMessageTemplateFct: loadingResultsMessageTemplateFct, 4511 onOpen: function(nodes) { 4512 if (config.isTabletAndUp) { 4513 return; 4514 } 4515 4516 var searchInputBoundingRect = $( 4517 selectors.searchInput 4518 )[0].getBoundingClientRect(); 4519 var bodyHeight = $('body').height(); 4520 var offset = 50; 4521 var resultsMaxHeight = 4522 bodyHeight - searchInputBoundingRect.bottom - offset; 4523 $(nodes.result).css({ 4524 maxHeight: resultsMaxHeight 4525 }); 4526 }, 4527 onBeforeDestroy: function(nodes) { 4528 // If the viewport width changes from mobile to tablet 4529 // reset the top position of the results 4530 $(nodes.result).css({ 4531 maxHeight: '' 4532 }); 4533 } 4534 }); 4535 4536 validateSearchHandler(searchInput, searchSubmit); 4537 } 4538 4539 function unload() { 4540 if (!componentInstance) { 4541 return; 4542 } 4543 componentInstance.destroy(); 4544 componentInstance = null; 4545 } 4546 4547 return { 4548 init: init, 4549 unload: unload 4550 }; 4551 })(); 4552 4553 window.theme.SearchHeader = (function() { 4554 var selectors = { 4555 searchInput: '[data-predictive-search-drawer-input]', 4556 searchResults: '[data-predictive-search-mount="drawer"]', 4557 searchFormContainer: '[data-search-form-container]', 4558 searchSubmit: '[data-search-form-submit]' 4559 }; 4560 4561 var componentInstance; 4562 var searchInput = document.querySelector(selectors.searchInput); 4563 var searchSubmit = document.querySelector(selectors.searchSubmit); 4564 4565 function init(config) { 4566 componentInstance = new window.Shopify.theme.PredictiveSearchComponent({ 4567 selectors: { 4568 input: selectors.searchInput, 4569 result: selectors.searchResults 4570 }, 4571 resultTemplateFct: window.theme.SearchResultsTemplate, 4572 numberOfResultsTemplateFct: numberOfResultsTemplateFct, 4573 numberOfResults: config.numberOfResults, 4574 loadingResultsMessageTemplateFct: loadingResultsMessageTemplateFct, 4575 onInputBlur: function() { 4576 return false; 4577 }, 4578 onOpen: function(nodes) { 4579 var searchInputBoundingRect = $( 4580 searchInput 4581 )[0].getBoundingClientRect(); 4582 4583 // For tablet screens and up, stop the scroll area from extending past 4584 // the bottom of the screen because we're locking the body scroll 4585 var maxHeight = 4586 window.innerHeight - 4587 searchInputBoundingRect.bottom - 4588 (config.isTabletAndUp ? 20 : 0); 4589 4590 $(nodes.result).css({ 4591 top: config.isTabletAndUp ? '' : searchInputBoundingRect.bottom, 4592 maxHeight: maxHeight 4593 }); 4594 }, 4595 onClose: function(nodes) { 4596 $(nodes.result).css({ 4597 maxHeight: '' 4598 }); 4599 }, 4600 onBeforeDestroy: function(nodes) { 4601 // If the viewport width changes from mobile to tablet 4602 // reset the top position of the results 4603 $(nodes.result).css({ 4604 top: '' 4605 }); 4606 } 4607 }); 4608 4609 validateSearchHandler(searchInput, searchSubmit); 4610 } 4611 4612 function unload() { 4613 if (!componentInstance) { 4614 return; 4615 } 4616 4617 componentInstance.destroy(); 4618 componentInstance = null; 4619 } 4620 4621 function clearAndClose() { 4622 if (!componentInstance) { 4623 return; 4624 } 4625 4626 componentInstance.clearAndClose(); 4627 } 4628 4629 return { 4630 init: init, 4631 unload: unload, 4632 clearAndClose: clearAndClose 4633 }; 4634 })(); 4635 4636 window.theme.Search = (function() { 4637 var classes = { 4638 searchTemplate: 'template-search' 4639 }; 4640 var selectors = { 4641 siteHeader: '.site-header' 4642 }; 4643 var mediaQueryList = { 4644 mobile: window.matchMedia('(max-width: 749px)'), 4645 tabletAndUp: window.matchMedia('(min-width: 750px)') 4646 }; 4647
4648 function init() { 4649 if (!$(selectors.siteHeader).length) { 4650 return; 4651 } 4652 4653 if (!canInitializePredictiveSearch()) { 4654 return; 4655 } 4656 4657 Object.keys(mediaQueryList).forEach(function(device) { 4658 mediaQueryList[device].addListener(initSearchAccordingToViewport); 4659 }); 4660 4661 initSearchAccordingToViewport(); 4662 } 4663 4664 function initSearchAccordingToViewport() { 4665 theme.SearchDrawer.close(); 4666 theme.SearchHeader.unload(); 4667 theme.SearchPage.unload(); 4668 4669 if (mediaQueryList.mobile.matches) { 4670 theme.SearchHeader.init({ 4671 numberOfResults: 4, 4672 isTabletAndUp: false 4673 }); 4674 4675 if (isSearchPage()) { 4676 theme.SearchPage.init({ isTabletAndUp: false }); 4677 } 4678 } else { 4679 // Tablet and up 4680 theme.SearchHeader.init({ 4681 numberOfResults: 4, 4682 isTabletAndUp: true 4683 }); 4684 4685 if (isSearchPage()) { 4686 theme.SearchPage.init({ isTabletAndUp: true }); 4687 } 4688 } 4689 } 4690 4691 function isSearchPage() {
4692 return $('body').hasClass(classes.searchTemplate); 4693 } 4694 4695 function unload() { 4696 theme.SearchHeader.unload(); 4697 theme.SearchPage.unload(); 4698 } 4699 4700 return { 4701 init: init, 4702 unload: unload 4703 }; 4704 })(); 4705})(); 4706 4707window.theme = window.theme || {}; 4708 4709theme.SearchDrawer = (function() { 4710 var selectors = { 4711 headerSection: '[data-header-section]', 4712 drawer: '[data-predictive-search-drawer]', 4713 drawerOpenButton: '[data-predictive-search-open-drawer]', 4714 headerSearchInput: '[data-predictive-search-drawer-input]', 4715 predictiveSearchWrapper: '[data-predictive-search-mount="drawer"]' 4716 }; 4717 4718 var drawerInstance; 4719 4720 function init() { 4721 setAccessibilityProps(); 4722 4723 drawerInstance = new theme.Drawers('SearchDrawer', 'top', { 4724 onDrawerOpen: function() { 4725 setHeight(); 4726 theme.MobileNav.closeMobileNav(); 4727 lockBodyScroll(); 4728 }, 4729 onDrawerClose: function() { 4730 theme.SearchHeader.clearAndClose(); 4731 $(selectors.drawerOpenButton).focus(); 4732 unlockBodyScroll(); 4733 }, 4734 withPredictiveSearch: true, 4735 $elementToFocusOnOpen: $(selectors.headerSearchInput) 4736 }); 4737 } 4738 4739 function setAccessibilityProps() { 4740 $(selectors.drawerOpenButton) 4741 .attr('aria-controls', 'SearchDrawer') 4742 .attr('aria-expanded', 'false') 4743 .attr('aria-haspopup', 'dialog'); 4744 } 4745 4746 function setHeight() { 4747 $(selectors.drawer).css({ 4748 height: $(selectors.headerSection).outerHeight() 4749 }); 4750 } 4751 4752 function close() { 4753 drawerInstance.close(); 4754 } 4755 4756 function lockBodyScroll() { 4757 // Disable scroll except on the predictive search container. 4758 window.bodyScrollLock.disableBodyScroll( 4759 document.querySelector(selectors.predictiveSearchWrapper), 4760 { 4761 allowTouchMove: function(element) { 4762 return ( 4763 // If the touch event is in an element under the predictive search 4764 // we allow don't prevent default. 4765 $(element).parents(selectors.predictiveSearchWrapper).length === 1 4766 ); 4767 } 4768 } 4769 ); 4770 } 4771 4772 function unlockBodyScroll() { 4773 window.bodyScrollLock.clearAllBodyScrollLocks(); 4774 } 4775 4776 return { 4777 init: init, 4778 close: close 4779 }; 4780})(); 4781 4782theme.Disclosure = (function() { 4783 var selectors = { 4784 disclosureList: '[data-disclosure-list]', 4785 disclosureToggle: '[data-disclosure-toggle]', 4786 disclosureInput: '[data-disclosure-input]', 4787 disclosureOptions: '[data-disclosure-option]' 4788 }; 4789 4790 var classes = { 4791 listVisible: 'disclosure-list--visible' 4792 }; 4793 4794 function Disclosure($disclosure) { 4795 this.$container = $disclosure; 4796 this.cache = {}; 4797 this._cacheSelectors(); 4798 this._connectOptions(); 4799 this._connectToggle(); 4800 this._onFocusOut(); 4801 } 4802 4803 Disclosure.prototype = _.assignIn({}, Disclosure.prototype, { 4804 _cacheSelectors: function() { 4805 this.cache = { 4806 $disclosureList: this.$container.find(selectors.disclosureList), 4807 $disclosureToggle: this.$container.find(selectors.disclosureToggle), 4808 $disclosureInput: this.$container.find(selectors.disclosureInput), 4809 $disclosureOptions: this.$container.find(selectors.disclosureOptions) 4810 }; 4811 }, 4812 4813 _connectToggle: function() { 4814 this.cache.$disclosureToggle.on( 4815 'click', 4816 function(evt) { 4817 var ariaExpanded = 4818 $(evt.currentTarget).attr('aria-expanded') === 'true'; 4819 $(evt.currentTarget).attr('aria-expanded', !ariaExpanded); 4820 4821 this.cache.$disclosureList.toggleClass(classes.listVisible); 4822 }.bind(this) 4823 ); 4824 }, 4825 4826 _connectOptions: function() { 4827 this.cache.$disclosureOptions.on( 4828 'click', 4829 function(evt) { 4830 this._submitForm($(evt.currentTarget).data('value')); 4831 }.bind(this) 4832 ); 4833 }, 4834 4835 _onFocusOut: function() { 4836 this.cache.$disclosureToggle.on( 4837 'focusout', 4838 function(evt) { 4839 var disclosureLostFocus = 4840 this.$container.has(evt.relatedTarget).length === 0; 4841 4842 if (disclosureLostFocus) { 4843 this._hideList(); 4844 } 4845 }.bind(this) 4846 ); 4847 4848 this.cache.$disclosureList.on(
4849 'focusout', 4850 function(evt) { 4851 var childInFocus = 4852 $(evt.currentTarget).has(evt.relatedTarget).length > 0; 4853 var isVisible = this.cache.$disclosureList.hasClass( 4854 classes.listVisible 4855 ); 4856 4857 if (isVisible && !childInFocus) { 4858 this._hideList(); 4859 } 4860 }.bind(this) 4861 ); 4862 4863 this.$container.on( 4864 'keyup', 4865 function(evt) { 4866 if (evt.which !== slate.utils.keyboardKeys.ESCAPE) return; 4867 this._hideList(); 4868 this.cache.$disclosureToggle.focus(); 4869 }.bind(this) 4870 ); 4871 4872 $('body').on( 4873 'click', 4874 function(evt) { 4875 var isOption = this.$container.has(evt.target).length > 0; 4876 var isVisible = this.cache.$disclosureList.hasClass( 4877 classes.listVisible 4878 ); 4879 4880 if (isVisible && !isOption) { 4881 this._hideList(); 4882 } 4883 }.bind(this) 4884 ); 4885 }, 4886 4887 _submitForm: function(value) { 4888 this.cache.$disclosureInput.val(value); 4889 this.$container.parents('form').submit(); 4890 }, 4891 4892 _hideList: function() { 4893 this.cache.$disclosureList.removeClass(classes.listVisible); 4894 this.cache.$disclosureToggle.attr('aria-expanded', false); 4895 }, 4896 4897 unload: function() { 4898 this.cache.$disclosureOptions.off(); 4899 this.cache.$disclosureToggle.off(); 4900 this.cache.$disclosureList.off(); 4901 this.$container.off(); 4902 } 4903 }); 4904 4905 return Disclosure; 4906})(); 4907 4908 4909/* ================ TEMPLATES ================ */ 4910(function() { 4911 var $filterBy = $('#BlogTagFilter'); 4912 4913 if (!$filterBy.length) { 4914 return; 4915 } 4916 4917 slate.utils.resizeSelects($filterBy); 4918 4919 $filterBy.on('change', function() { 4920 location.href = $(this).val(); 4921 }); 4922})(); 4923 4924window.theme = theme || {}; 4925 4926theme.customerTemplates = (function() { 4927 var selectors = { 4928 RecoverHeading: '#RecoverHeading', 4929 RecoverEmail: '#RecoverEmail', 4930 LoginHeading: '#LoginHeading' 4931 }; 4932 4933 function initEventListeners() { 4934 this.$RecoverHeading = $(selectors.RecoverHeading); 4935 this.$RecoverEmail = $(selectors.RecoverEmail); 4936 this.$LoginHeading = $(selectors.LoginHeading); 4937 4938 // Show reset password form 4939 $('#RecoverPassword').on( 4940 'click', 4941 function(evt) { 4942 evt.preventDefault(); 4943 showRecoverPasswordForm(); 4944 this.$RecoverHeading.attr('tabindex', '-1').focus(); 4945 }.bind(this) 4946 ); 4947 4948 // Hide reset password form 4949 $('#HideRecoverPasswordLink').on( 4950 'click', 4951 function(evt) { 4952 evt.preventDefault(); 4953 hideRecoverPasswordForm(); 4954 this.$LoginHeading.attr('tabindex', '-1').focus(); 4955 }.bind(this) 4956 ); 4957 4958 this.$RecoverHeading.on('blur', function() { 4959 $(this).removeAttr('tabindex'); 4960 }); 4961 4962 this.$LoginHeading.on('blur', function() { 4963 $(this).removeAttr('tabindex'); 4964 }); 4965 } 4966 4967 /** 4968 * 4969 * Show/Hide recover password form 4970 * 4971 */ 4972 4973 function showRecoverPasswordForm() { 4974 $('#RecoverPasswordForm').removeClass('hide'); 4975 $('#CustomerLoginForm').addClass('hide'); 4976 4977 if (this.$RecoverEmail.attr('aria-invalid') === 'true') { 4978 this.$RecoverEmail.focus(); 4979 } 4980 } 4981 4982 function hideRecoverPasswordForm() { 4983 $('#RecoverPasswordForm').addClass('hide'); 4984 $('#CustomerLoginForm').removeClass('hide'); 4985 } 4986 4987 /** 4988 * 4989 * Show reset password success message 4990 * 4991 */ 4992 function resetPasswordSuccess() { 4993 var $formState = $('.reset-password-success'); 4994 4995 // check if reset password form was successfully submited. 4996 if (!$formState.length) { 4997 return; 4998 } 4999 5000 // show success message 5001 $('#ResetSuccess') 5002 .removeClass('hide') 5003 .focus(); 5004 } 5005 5006 /** 5007 * 5008 * Show/hide customer address forms 5009 * 5010 */ 5011 function customerAddressForm() { 5012 var $newAddressForm = $('#AddressNewForm'); 5013 var $newAddressFormButton = $('#AddressNewButton'); 5014 5015 if (!$newAddressForm.length) { 5016 return; 5017 } 5018 5019 // Initialize observers on address selectors, defined in shopify_common.js 5020 if (Shopify) { 5021 // eslint-disable-next-line no-new 5022 new Shopify.CountryProvinceSelector( 5023 'AddressCountryNew', 5024 'AddressProvinceNew', 5025 { 5026 hideElement: 'AddressProvinceContainerNew' 5027 } 5028 ); 5029 } 5030
5031 // Initialize each edit form's country/province selector 5032 $('.address-country-option').each(function() { 5033 var formId = $(this).data('form-id'); 5034 var countrySelector = 'AddressCountry_' + formId; 5035 var provinceSelector = 'AddressProvince_' + formId; 5036 var containerSelector = 'AddressProvinceContainer_' + formId; 5037 5038 // eslint-disable-next-line no-new 5039 new Shopify.CountryProvinceSelector(countrySelector, provinceSelector, { 5040 hideElement: containerSelector 5041 }); 5042 }); 5043 5044 // Toggle new/edit address forms 5045 $('.address-new-toggle').on('click', function() { 5046 var isExpanded = $newAddressFormButton.attr('aria-expanded') === 'true'; 5047 5048 $newAddressForm.toggleClass('hide'); 5049 $newAddressFormButton.attr('aria-expanded', !isExpanded).focus(); 5050 }); 5051 5052 $('.address-edit-toggle').on('click', function() { 5053 var formId = $(this).data('form-id'); 5054 var $editButton = $('#EditFormButton_' + formId); 5055 var $editAddress = $('#EditAddress_' + formId); 5056 var isExpanded = $editButton.attr('aria-expanded') === 'true'; 5057 5058 $editAddress.toggleClass('hide'); 5059 $editButton.attr('aria-expanded', !isExpanded).focus(); 5060 }); 5061 5062 $('.address-delete').on('click', function() { 5063 var $el = $(this); 5064 var target = $el.data('target'); 5065 var confirmMessage = $el.data('confirm-message'); 5066 5067 // eslint-disable-next-line no-alert 5068 if ( 5069 confirm( 5070 confirmMessage || 'Are you sure you wish to delete this address?' 5071 ) 5072 ) { 5073 Shopify.postLink(target, { 5074 parameters: { _method: 'delete' } 5075 }); 5076 } 5077 }); 5078 } 5079 5080 /** 5081 * 5082 * Check URL for reset password hash 5083 * 5084 */ 5085 function checkUrlHash() { 5086 var hash = window.location.hash; 5087 5088 // Allow deep linking to recover password form 5089 if (hash === '#recover') { 5090 showRecoverPasswordForm.bind(this)(); 5091 } 5092 } 5093 5094 return { 5095 init: function() { 5096 initEventListeners(); 5097 checkUrlHash(); 5098 resetPasswordSuccess(); 5099 customerAddressForm(); 5100 } 5101 }; 5102})(); 5103 5104 5105/*================ SECTIONS ================*/ 5106window.theme = window.theme || {}; 5107 5108theme.Cart = (function() { 5109 var selectors = { 5110 cartCount: '[data-cart-count]', 5111 cartCountBubble: '[data-cart-count-bubble]', 5112 cartDiscount: '[data-cart-discount]', 5113 cartDiscountTitle: '[data-cart-discount-title]', 5114 cartDiscountAmount: '[data-cart-discount-amount]', 5115 cartDiscountWrapper: '[data-cart-discount-wrapper]', 5116 cartErrorMessage: '[data-cart-error-message]', 5117 cartErrorMessageWrapper: '[data-cart-error-message-wrapper]', 5118 cartItem: '[data-cart-item]', 5119 cartItemDetails: '[data-cart-item-details]', 5120 cartItemDiscount: '[data-cart-item-discount]', 5121 cartItemDiscountedPriceGroup: '[data-cart-item-discounted-price-group]', 5122 cartItemDiscountTitle: '[data-cart-item-discount-title]', 5123 cartItemDiscountAmount: '[data-cart-item-discount-amount]', 5124 cartItemDiscountList: '[data-cart-item-discount-list]', 5125 cartItemFinalPrice: '[data-cart-item-final-price]', 5126 cartItemImage: '[data-cart-item-image]', 5127 cartItemLinePrice: '[data-cart-item-line-price]', 5128 cartItemOriginalPrice: '[data-cart-item-original-price]', 5129 cartItemPrice: '[data-cart-item-price]', 5130 cartItemPriceList: '[data-cart-item-price-list]', 5131 cartItemProperty: '[data-cart-item-property]', 5132 cartItemPropertyName: '[data-cart-item-property-name]', 5133 cartItemPropertyValue: '[data-cart-item-property-value]', 5134 cartItemRegularPriceGroup: '[data-cart-item-regular-price-group]', 5135 cartItemRegularPrice: '[data-cart-item-regular-price]', 5136 cartItemTitle: '[data-cart-item-title]', 5137 cartItemOption: '[data-cart-item-option]', 5138 cartLineItems: '[data-cart-line-items]', 5139 cartNote: '[data-cart-notes]', 5140 cartQuantityErrorMessage: '[data-cart-quantity-error-message]', 5141 cartQuantityErrorMessageWrapper: 5142 '[data-cart-quantity-error-message-wrapper]', 5143 cartRemove: '[data-cart-remove]', 5144 cartStatus: '[data-cart-status]', 5145 cartSubtotal: '[data-cart-subtotal]', 5146 cartTableCell: '[data-cart-table-cell]', 5147 cartWrapper: '[data-cart-wrapper]', 5148 emptyPageContent: '[data-empty-page-content]', 5149 quantityInput: '[data-quantity-input]', 5150 quantityInputMobile: '[data-quantity-input-mobile]', 5151 quantityInputDesktop: '[data-quantity-input-desktop]', 5152 quantityLabelMobile: '[data-quantity-label-mobile]', 5153 quantityLabelDesktop: '[data-quantity-label-desktop]', 5154 inputQty: '[data-quantity-input]', 5155 thumbnails: '.cart__image', 5156 unitPrice: '[data-unit-price]', 5157 unitPriceBaseUnit: '[data-unit-price-base-unit]', 5158 unitPriceGroup: '[data-unit-price-group]' 5159 }; 5160 5161 var classes = {
5162 cartNoCookies: 'cart--no-cookies', 5163 cartRemovedProduct: 'cart__removed-product', 5164 hide: 'hide', 5165 inputError: 'input--error' 5166 }; 5167 5168 var attributes = { 5169 cartItemIndex: 'data-cart-item-index', 5170 cartItemKey: 'data-cart-item-key', 5171 cartItemQuantity: 'data-cart-item-quantity', 5172 cartItemTitle: 'data-cart-item-title', 5173 cartItemUrl: 'data-cart-item-url', 5174 quantityItem: 'data-quantity-item' 5175 }; 5176 5177 theme.breakpoints = theme.breakpoints || {}; 5178 5179 if ( 5180 isNaN(theme.breakpoints.medium) || 5181 theme.breakpoints.medium === undefined 5182 ) { 5183 theme.breakpoints.medium = 750; 5184 } 5185 5186 var mediumUpQuery = '(min-width: ' + theme.breakpoints.medium + 'px)'; 5187 5188 function Cart(container) { 5189 this.$container = $(container); 5190 this.$thumbnails = $(selectors.thumbnails, this.$container); 5191 this.ajaxEnabled = this.$container.data('ajax-enabled'); 5192 5193 if (!this.cookiesEnabled()) { 5194 this.$container.addClass(classes.cartNoCookies); 5195 } 5196 5197 this.$thumbnails.css('cursor', 'pointer'); 5198 this.$container.on( 5199 'click', 5200 selectors.thumbnails, 5201 this._handleThumbnailClick 5202 ); 5203 5204 this.$container.on( 5205 'change', 5206 selectors.inputQty, 5207 $.debounce(500, this._handleInputQty.bind(this)) 5208 ); 5209 5210 this.mql = window.matchMedia(mediumUpQuery); 5211 this.mql.addListener(this.setQuantityFormControllers.bind(this)); 5212 this.setQuantityFormControllers(); 5213 5214 if (this.ajaxEnabled) { 5215 /** 5216 * Because the entire cart is recreated when a cart item is updated, 5217 * we cannot cache the elements in the cart. Instead, we add the event 5218 * listeners on the cart's container to allow us to retain the event 5219 * listeners after rebuilding the cart when an item is updated. 5220 */ 5221 5222 this.$container.on( 5223 'change', 5224 selectors.cartNote, 5225 this._onNoteChange.bind(this) 5226 ); 5227 5228 this.$container.on( 5229 'click', 5230 selectors.cartRemove, 5231 this._onRemoveItem.bind(this) 5232 ); 5233 5234 this._setupCartTemplates(); 5235 } 5236 } 5237 5238 Cart.prototype = _.assignIn({}
5238, Cart.prototype, { 5239 _setupCartTemplates: function() { 5240 this.$itemTemplate = $(selectors.cartItem, this.$container) 5241 .first() 5242 .clone(); 5243 this.$itemDiscountTemplate = $( 5244 selectors.cartItemDiscount, 5245 this.$itemTemplate 5246 ).clone(); 5247 this.$itemOptionTemplate = $( 5248 selectors.cartItemOption, 5249 this.$itemTemplate 5250 ).clone(); 5251 this.$itemPropertyTemplate = $( 5252 selectors.cartItemProperty, 5253 this.$itemTemplate 5254 ).clone(); 5255 this.$itemPriceListTemplate = $( 5256 selectors.cartItemPriceList, 5257 this.$itemTemplate 5258 ).clone(); 5259 this.$itemLinePriceTemplate = $( 5260 selectors.cartItemLinePrice, 5261 this.$itemTemplate 5262 ).clone(); 5263 this.$cartDiscountTemplate = $( 5264 selectors.cartDiscount, 5265 this.$container 5266 ).clone(); 5267 }, 5268 5269 _handleInputQty: function(evt) { 5270 var $input = $(evt.target); 5271 var itemIndex = $input.data('quantity-item'); 5272 var $itemElement = $input.closest(selectors.cartItem); 5273 var $itemQtyInputs = $('[data-quantity-item=' + itemIndex + ']'); 5274 var value = parseInt($input.val()); 5275 var isValidValue = !(value < 0 || isNaN(value)); 5276 $itemQtyInputs.val(value); 5277 5278 this._hideCartError(); 5279 this._hideQuantityErrorMessage(); 5280 5281 if (!isValidValue) { 5282 this._showQuantityErrorMessages($itemElement); 5283 return; 5284 } 5285 5286 if (isValidValue && this.ajaxEnabled) { 5287 this._updateItemQuantity( 5288 itemIndex, 5289 $itemElement, 5290 $itemQtyInputs, 5291 value 5292 ); 5293 } 5294 }, 5295 5296 _updateItemQuantity: function( 5297 itemIndex, 5298 $itemElement, 5299 $itemQtyInputs, 5300 value 5301 ) { 5302 var key = $itemElement.attr(attributes.cartItemKey); 5303 var index = $itemElement.attr(attributes.cartItemIndex); 5304 5305 var params = { 5306 url: '/cart/change.js', 5307 data: { quantity: value, line: index }, 5308 dataType: 'json' 5309 }; 5310 5311 $.post(params) 5312 .done( 5313 function(state) { 5314 if (state.item_count === 0) { 5315 this._emptyCart(); 5316 } else { 5317 this._createCart(state); 5318 5319 if (value === 0) { 5320 this._showRemoveMessage($itemElement.clone()); 5321 } else { 5322 var $lineItem = $('[data-cart-item-key="' + key + '"]'); 5323 var item = this.getItem(key, state); 5324 5325 $(selectors.quantityInput, $lineItem).focus(); 5326 this._updateLiveRegion(item); 5327 } 5328 } 5329 5330 this._setCartCountBubble(state.item_count); 5331 }.bind(this) 5332 ) 5333 .fail( 5334 function() { 5335 this._showCartError($itemQtyInputs); 5336 }.bind(this) 5337 ); 5338 }, 5339 5340 getItem: function(key, state) { 5341 return state.items.find(function(item) { 5342 return item.key === key; 5343 }); 5344 }, 5345 5346 _liveRegionText: function(item) { 5347 // Dummy content for live region 5348 var liveRegionText = 5349 theme.strings.update + 5350 ': [QuantityLabel]: [Quantity], [Regular] [$$] [DiscountedPrice] [$]. [PriceInformation]'; 5351 5352 // Update Quantity 5353 liveRegionText = liveRegionText 5354 .replace('[QuantityLabel]', theme.strings.quantity) 5355 .replace('[Quantity]', item.quantity); 5356 5357 // Update pricing information 5358 var regularLabel = ''; 5359 var regularPrice = theme.Currency.formatMoney( 5360 item.original_line_price, 5361 theme.moneyFormat 5362 ); 5363 var discountLabel = ''; 5364 var discountPrice = ''; 5365 var discountInformation = ''; 5366 5367 if (item.original_line_price > item.final_line_price) { 5368 regularLabel = theme.strings.regularTotal; 5369 5370 discountLabel = theme.strings.discountedTotal; 5371 discountPrice = theme.Currency.formatMoney( 5372 item.final_line_price, 5373 theme.moneyFormat 5374 ); 5375 5376 discountInformation = theme.strings.priceColumn; 5377 } 5378 5379 liveRegionText = liveRegionText 5380 .replace('[Regular]', regularLabel) 5381 .replace('[$$]', regularPrice) 5382 .replace('[DiscountedPrice]', discountLabel) 5383 .replace('[$]', discountPrice) 5384 .replace('[PriceInformation]', discountInformation) 5385 .trim(); 5386 5387 return liveRegionText; 5388 }, 5389 5390 _updateLiveRegion: function(item) { 5391 var $liveRegion = $(selectors.cartStatus); 5392 $liveRegion.html(this._liveRegionText(item)).attr('aria-hidden', false); 5393 5394 // hide content from accessibility tree after announcement 5395 setTimeout(function() { 5396 $liveRegion.attr('aria-hidden', true); 5397 }, 1000); 5398 }, 5399 5400 _createCart: function(state) { 5401 var cartDiscountList = this._createCartDiscountList(state); 5402 5403 $(selectors.cartLineItems, this.$container).html( 5404 this._createLineItemList(state) 5405 ); 5406 5407 this.setQuantityFormControllers(); 5408 5409 $(selectors.cartNote, this.$container).val(state.note); 5410 5411 if (cartDiscountList.length === 0) { 5412 $(selectors.cartDiscountWrapper, this.$container) 5413 .html('')
5414 .addClass(classes.hide); 5415 } else { 5416 $(selectors.cartDiscountWrapper, this.$container) 5417 .html(cartDiscountList) 5418 .removeClass(classes.hide); 5419 } 5420 5421 $(selectors.cartSubtotal, this.$container).html( 5422 theme.Currency.formatMoney( 5423 state.total_price, 5424 theme.moneyFormatWithCurrency 5425 ) 5426 ); 5427 }, 5428 5429 _createCartDiscountList: function(cart) { 5430 return $.map( 5431 cart.cart_level_discount_applications, 5432 function(discount) { 5433 var $discount = this.$cartDiscountTemplate.clone(); 5434 $discount.find(selectors.cartDiscountTitle).text(discount.title); 5435 $discount 5436 .find(selectors.cartDiscountAmount) 5437 .html( 5438 theme.Currency.formatMoney( 5439 discount.total_allocated_amount, 5440 theme.moneyFormat 5441 ) 5442 ); 5443 return $discount[0]; 5444 }.bind(this) 5445 ); 5446 }, 5447 5448 _createLineItemList: function(state) { 5449 return $.map( 5450 state.items, 5451 function(item, index) { 5452 var $item = this.$itemTemplate.clone(); 5453 var $itemPriceList = this.$itemPriceListTemplate.clone(); 5454 5455 this._setLineItemAttributes($item, item, index); 5456 this._setLineItemImage($item, item.featured_image); 5457 5458 $(selectors.cartItemTitle, $item) 5459 .text(item.product_title) 5460 .attr('href', item.url); 5461 5462 var productDetailsList = this._createProductDetailsList( 5463 item.product_has_only_default_variant, 5464 item.options_with_values, 5465 item.properties 5466 ); 5467 this._setProductDetailsList($item, productDetailsList); 5468 5469 this._setItemRemove($item, item.title); 5470 5471 $itemPriceList.html( 5472 this._createItemPrice( 5473 item.original_price, 5474 item.final_price, 5475 this.$itemPriceListTemplate 5476 ) 5477 ); 5478 5479 if (item.unit_price_measurement) { 5480 $itemPriceList.append( 5481 this._createUnitPrice( 5482 item.unit_price, 5483 item.unit_price_measurement, 5484 this.$itemPriceListTemplate 5485 ) 5486 ); 5487 } 5488 5489 this._setItemPrice($item, $itemPriceList); 5490 5491 var itemDiscountList = this._createItemDiscountList(item); 5492 this._setItemDiscountList($item, itemDiscountList); 5493 5494 this._setQuantityInputs($item, item, index); 5495 5496 var itemLinePrice = this._createItemPrice( 5497 item.original_line_price, 5498 item.final_line_price, 5499 this.$itemLinePriceTemplate 5500 ); 5501 this._setItemLinePrice($item, itemLinePrice); 5502 5503 return $item[0]; 5504 }.bind(this) 5505 ); 5506 }, 5507 5508 _setLineItemAttributes: function($item, item, index) { 5509 $item 5510 .attr(attributes.cartItemKey, item.key) 5511 .attr(attributes.cartItemUrl, item.url) 5512 .attr(attributes.cartItemTitle, item.title) 5513 .attr(attributes.cartItemIndex, index + 1) 5514 .attr(attributes.cartItemQuantity, item.quantity); 5515 }, 5516 5517 _setLineItemImage: function($item, featuredImage) { 5518 var $image = $(selectors.cartItemImage, $item); 5519 5520 var sizedImageUrl = 5521 featuredImage.url !== null 5522 ? theme.Images.getSizedImageUrl(featuredImage.url, 'x190') 5523 : null; 5524 5525 if (sizedImageUrl) { 5526 $image 5527 .attr('alt', featuredImage.alt) 5528 .attr('src', sizedImageUrl) 5529 .removeClass(classes.hide); 5530 } else { 5531 $image.remove(); 5532 } 5533 }, 5534 5535 _setProductDetailsList: function($item, productDetailsList) { 5536 var $itemDetails = $(selectors.cartItemDetails, $item); 5537 5538 if (productDetailsList.length === 0) {
5539 $itemDetails.addClass(classes.hide).text(''); 5540 } else { 5541 $itemDetails.removeClass(classes.hide).html(productDetailsList); 5542 } 5543 }, 5544 5545 _setItemPrice: function($item, price) { 5546 $(selectors.cartItemPrice, $item).html(price); 5547 }, 5548 5549 _setItemDiscountList: function($item, discountList) { 5550 var $itemDiscountList = $(selectors.cartItemDiscountList, $item); 5551 5552 if (discountList.length === 0) { 5553 $itemDiscountList.html('').addClass(classes.hide); 5554 } else { 5555 $itemDiscountList.html(discountList).removeClass(classes.hide); 5556 } 5557 }, 5558 5559 _setItemRemove: function($item, title) { 5560 $(selectors.cartRemove, $item).attr( 5561 'aria-label', 5562 theme.strings.removeLabel.replace('[product]', title) 5563 ); 5564 }, 5565 5566 _setQuantityInputs: function($item, item, index) { 5567 $(selectors.quantityInputMobile, $item) 5568 .attr('id', 'updates_' + item.key) 5569 .attr(attributes.quantityItem, index + 1) 5570 .val(item.quantity); 5571 5572 $(selectors.quantityInputDesktop, $item) 5573 .attr('id', 'updates_large_' + item.key) 5574 .attr(attributes.quantityItem, index + 1) 5575 .val(item.quantity); 5576 5577 $(selectors.quantityLabelMobile, $item).attr( 5578 'for', 5579 'updates_' + item.key 5580 ); 5581 5582 $(selectors.quantityLabelDesktop, $item).attr( 5583 'for', 5584 'updates_large_' + item.key 5585 ); 5586 }, 5587 5588 setQuantityFormControllers: function() { 5589 if (this.mql.matches) { 5590 $(selectors.quantityInputDesktop).attr('name', 'updates[]'); 5591 $(selectors.quantityInputMobile).removeAttr('name'); 5592 } else { 5593 $(selectors.quantityInputMobile).attr('name', 'updates[]'); 5594 $(selectors.quantityInputDesktop).removeAttr('name'); 5595 } 5596 }, 5597 5598 _setItemLinePrice: function($item, price) { 5599 $(selectors.cartItemLinePrice, $item).html(price); 5600 }, 5601 5602 _createProductDetailsList: function( 5603 product_has_only_default_variant, 5604 options, 5605 properties 5606 ) { 5607 var optionsPropertiesHTML = []; 5608 5609 if (!product_has_only_default_variant) { 5610 optionsPropertiesHTML = optionsPropertiesHTML.concat( 5611 this._getOptionList(options) 5612 ); 5613 } 5614 5615 if (properties !== null && Object.keys(properties).length !== 0) { 5616 optionsPropertiesHTML = optionsPropertiesHTML.concat( 5617 this._getPropertyList(properties) 5618 ); 5619 } 5620 5621 return optionsPropertiesHTML; 5622 }, 5623 5624 _getOptionList: function(options) { 5625 return $.map( 5626 options, 5627 function(option) { 5628 var $optionElement = this.$itemOptionTemplate.clone(); 5629 5630 $optionElement 5631 .text(option.name + ': ' + option.value) 5632 .removeClass(classes.hide); 5633 5634 return $optionElement[0]; 5635 }.bind(this) 5636 ); 5637 }, 5638 5639 _getPropertyList: function(properties) { 5640 var propertiesArray = 5641 properties !== null ? Object.entries(properties) : []; 5642 5643 return $.map( 5644 propertiesArray, 5645 function(property) { 5646 var $propertyElement = this.$itemPropertyTemplate.clone(); 5647 5648 // Line item properties prefixed with an underscore are not to be displayed 5649 if (property[0].charAt(0) === '_') return; 5650 5651 // if the property value has a length of 0 (empty), don't display it 5652 if (property[1].length === 0) return; 5653 5654 $propertyElement 5655 .find(selectors.cartItemPropertyName) 5656 .text(property[0]); 5657 5658 if (property[0].indexOf('/uploads/') === -1) { 5659 $propertyElement 5660 .find(selectors.cartItemPropertyValue) 5661 .text(': ' + property[1]); 5662 } else { 5663 $propertyElement 5664 .find(selectors.cartItemPropertyValue) 5665 .html( 5666 ': <a href="' + 5667 property[1] + 5668 '"> ' + 5669 property[1].split('/').pop() + 5670 '</a>' 5671 ); 5672 } 5673 5674 $propertyElement.removeClass(classes.hide); 5675 5676 return $propertyElement[0]; 5677 }.bind(this) 5678 ); 5679 }, 5680 5681 _createItemPrice: function(original_price, final_price, $priceTemplate) { 5682 if (original_price !== final_price) { 5683 var $discountedPrice = $( 5684 selectors.cartItemDiscountedPriceGroup, 5685 $priceTemplate 5686 ).clone(); 5687 5688 $(selectors.cartItemOriginalPrice, $discountedPrice).html( 5689 theme.Currency.formatMoney(original_price, theme.moneyFormat) 5690 ); 5691 $(selectors.cartItemFinalPrice, $discountedPrice).html( 5692 theme.Currency.formatMoney(final_price, theme.moneyFormat) 5693 );
5694 $discountedPrice.removeClass(classes.hide); 5695 5696 return $discountedPrice[0]; 5697 } else { 5698 var $regularPrice = $( 5699 selectors.cartItemRegularPriceGroup, 5700 $priceTemplate 5701 ).clone(); 5702 5703 $(selectors.cartItemRegularPrice, $regularPrice).html( 5704 theme.Currency.formatMoney(original_price, theme.moneyFormat) 5705 ); 5706 5707 $regularPrice.removeClass(classes.hide); 5708 5709 return $regularPrice[0]; 5710 } 5711 }, 5712 5713 _createUnitPrice: function( 5714 unitPrice, 5715 unitPriceMeasurement, 5716 $itemPriceGroup 5717 ) { 5718 var $unitPriceGroup = $( 5719 selectors.unitPriceGroup, 5720 $itemPriceGroup 5721 ).clone(); 5722 5723 var unitPriceBaseUnit = 5724 (unitPriceMeasurement.reference_value !== 1 5725 ? unitPriceMeasurement.reference_value 5726 : '') + unitPriceMeasurement.reference_unit; 5727 5728 $(selectors.unitPriceBaseUnit, $unitPriceGroup).text(unitPriceBaseUnit); 5729 $(selectors.unitPrice, $unitPriceGroup).html( 5730 theme.Currency.formatMoney(unitPrice, theme.moneyFormat) 5731 ); 5732 5733 $unitPriceGroup.removeClass(classes.hide); 5734 5735 return $unitPriceGroup[0]; 5736 }, 5737 5738 _createItemDiscountList: function(item) { 5739 return $.map( 5740 item.line_level_discount_allocations, 5741 function(discount) { 5742 var $discount = this.$itemDiscountTemplate.clone(); 5743 $discount 5744 .find(selectors.cartItemDiscountTitle) 5745 .text(discount.discount_application.title); 5746 $discount 5747 .find(selectors.cartItemDiscountAmount) 5748 .html( 5749 theme.Currency.formatMoney(discount.amount, theme.moneyFormat) 5750 ); 5751 return $discount[0]; 5752 }.bind(this) 5753 ); 5754 }, 5755 5756 _showQuantityErrorMessages: function(itemElement) { 5757 $(selectors.cartQuantityErrorMessage, itemElement).text( 5758 theme.strings.quantityMinimumMessage 5759 ); 5760 5761 $(selectors.cartQuantityErrorMessageWrapper, itemElement).removeClass( 5762 classes.hide 5763 ); 5764 5765 $(selectors.inputQty, itemElement)
5766 .addClass(classes.inputError) 5767 .focus(); 5768 }, 5769 5770 _hideQuantityErrorMessage: function() { 5771 var $errorMessages = $( 5772 selectors.cartQuantityErrorMessageWrapper 5773 ).addClass(classes.hide); 5774 5775 $(selectors.cartQuantityErrorMessage, $errorMessages).text(''); 5776 5777 $(selectors.inputQty, this.$container).removeClass(classes.inputError); 5778 }, 5779 5780 _handleThumbnailClick: function(evt) { 5781 var url = $(evt.target) 5782 .closest(selectors.cartItem) 5783 .data('cart-item-url'); 5784 5785 window.location.href = url; 5786 }, 5787 5788 _onNoteChange: function(evt) { 5789 var note = evt.currentTarget.value; 5790 this._hideCartError(); 5791 this._hideQuantityErrorMessage(); 5792 5793 var params = { 5794 url: '/cart/update.js', 5795 data: { note: note }, 5796 dataType: 'json' 5797 }; 5798 5799 $.post(params).fail( 5800 function() { 5801 this._showCartError(evt.currentTarget); 5802 }.bind(this) 5803 ); 5804 }, 5805 5806 _showCartError: function(elementToFocus) { 5807 $(selectors.cartErrorMessage).text(theme.strings.cartError); 5808 5809 $(selectors.cartErrorMessageWrapper).removeClass(classes.hide); 5810 5811 elementToFocus.focus(); 5812 }, 5813 5814 _hideCartError: function() {
5815 $(selectors.cartErrorMessageWrapper).addClass(classes.hide); 5816 $(selectors.cartErrorMessage).text(''); 5817 }, 5818 5819 _onRemoveItem: function(evt) { 5820 evt.preventDefault(); 5821 var $remove = $(evt.target); 5822 var $lineItem = $remove.closest(selectors.cartItem); 5823 var index = $lineItem.attr(attributes.cartItemIndex); 5824 this._hideCartError(); 5825 5826 var params = { 5827 url: '/cart/change.js', 5828 data: { quantity: 0, line: index }, 5829 dataType: 'json' 5830 }; 5831 5832 $.post(params) 5833 .done( 5834 function(state) { 5835 if (state.item_count === 0) { 5836 this._emptyCart(); 5837 } else { 5838 this._createCart(state); 5839 this._showRemoveMessage($lineItem.clone()); 5840 } 5841 5842 this._setCartCountBubble(state.item_count); 5843 }.bind(this) 5844 ) 5845 .fail( 5846 function() { 5847 this._showCartError(null); 5848 }.bind(this) 5849 ); 5850 }, 5851 5852 _showRemoveMessage: function(lineItem) { 5853 var index = lineItem.data('cart-item-index'); 5854 var removeMessage = this._getRemoveMessage(lineItem); 5855 var $lineItemAtIndex; 5856 5857 if (index - 1 === 0) { 5858 $lineItemAtIndex = $('[data-cart-item-index="1"]', this.$container); 5859 $(removeMessage).insertBefore($lineItemAtIndex); 5860 } else { 5861 $lineItemAtIndex = $( 5862 '[data-cart-item-index="' + (index - 1) + '"]', 5863 this.$container 5864 ); 5865 removeMessage.insertAfter($lineItemAtIndex); 5866 } 5867 removeMessage.focus(); 5868 }, 5869 5870 _getRemoveMessage: function(lineItem) { 5871 var formattedMessage = this._formatRemoveMessage(lineItem); 5872 5873 var $tableCell = $(selectors.cartTableCell, lineItem).clone(); 5874 $tableCell 5875 .removeClass()
5876 .addClass(classes.cartRemovedProduct) 5877 .attr('colspan', '4') 5878 .html(formattedMessage); 5879 5880 lineItem 5881 .attr('role', 'alert') 5882 .html($tableCell) 5883 .attr('tabindex', '-1'); 5884 5885 return lineItem; 5886 }, 5887 5888 _formatRemoveMessage: function(lineItem) { 5889 var quantity = lineItem.data('cart-item-quantity'); 5890 var url = lineItem.attr(attributes.cartItemUrl); 5891 var title = lineItem.attr(attributes.cartItemTitle); 5892 5893 return theme.strings.removedItemMessage 5894 .replace('[quantity]', quantity) 5895 .replace( 5896 '[link]', 5897 '<a ' + 5898 'href="' + 5899 url + 5900 '" class="text-link text-link--accent">' + 5901 title + 5902 '</a>' 5903 ); 5904 }, 5905 5906 _setCartCountBubble: function(quantity) { 5907 this.$cartCountBubble = 5908 this.$cartCountBubble || $(selectors.cartCountBubble); 5909 this.$cartCount = this.$cartCount || $(selectors.cartCount); 5910 5911 if (quantity > 0) { 5912 this.$cartCountBubble.removeClass(classes.hide); 5913 this.$cartCount.html(quantity); 5914 } else {
5915 this.$cartCountBubble.addClass(classes.hide); 5916 this.$cartCount.html(''); 5917 } 5918 }, 5919 5920 _emptyCart: function() { 5921 this.$emptyPageContent = 5922 this.$emptyPageContent || 5923 $(selectors.emptyPageContent, this.$container); 5924 this.$cartWrapper = 5925 this.$cartWrapper || $(selectors.cartWrapper, this.$container); 5926 5927 this.$emptyPageContent.removeClass(classes.hide); 5928 this.$cartWrapper.addClass(classes.hide); 5929 }, 5930 5931 cookiesEnabled: function() { 5932 var cookieEnabled = navigator.cookieEnabled; 5933 5934 if (!cookieEnabled) { 5935 document.cookie = 'testcookie'; 5936 cookieEnabled = document.cookie.indexOf('testcookie') !== -1; 5937 } 5938 return cookieEnabled; 5939 } 5940 }); 5941 5942 return Cart; 5943})(); 5944 5945window.theme = window.theme || {}; 5946 5947theme.Filters = (function() { 5948 var settings = { 5949 // Breakpoints from src/stylesheets/global/variables.scss.liquid 5950 mediaQueryMediumUp: 'screen and (min-width: 750px)' 5951 }; 5952 5953 var selectors = { 5954 mainContent: '#MainContent', 5955 filterSelection: '#FilterTags', 5956 sortSelection: '#SortBy' 5957 }; 5958 5959 var data = { 5960 sortBy: 'data-default-sortby' 5961 }; 5962 5963 function Filters(container) { 5964 var $container = (this.$container = $(container)); 5965 5966 this.$filterSelect = $(selectors.filterSelection, $container); 5967 this.$sortSelect = $(selectors.sortSelection, $container); 5968 this.$selects = $(selectors.filterSelection, $container).add( 5969 $(selectors.sortSelection, $container) 5970 ); 5971 5972 this.defaultSort = this._getDefaultSortValue(); 5973 this.$selects.removeClass('hidden'); 5974 5975 this.$filterSelect.on('change', this._onFilterChange.bind(this)); 5976 this.$sortSelect.on('change', this._onSortChange.bind(this)); 5977 this._initBreakpoints(); 5978 this._initParams(); 5979 } 5980 5981 Filters.prototype = _.assignIn({}, Filters.prototype, { 5982 _initBreakpoints: function() { 5983 var self = this; 5984 5985 enquire.register(settings.mediaQueryMediumUp, { 5986 match: function() { 5987 slate.utils.resizeSelects(self.$selects); 5988 } 5989 }); 5990 }, 5991 5992 _initParams: function() { 5993 self.queryParams = {}; 5994 if (location.search.length) { 5995 var aKeyValue; 5996 var aCouples = location.search.substr(1).split('&'); 5997 for (var i = 0; i < aCouples.length; i++) { 5998 aKeyValue = aCouples[i].split('='); 5999 if (aKeyValue.length > 1) { 6000 self.queryParams[ 6001 decodeURIComponent(aKeyValue[0]) 6002 ] = decodeURIComponent(aKeyValue[1]); 6003 } 6004 } 6005 } 6006 }, 6007 6008 _onSortChange: function() { 6009 self.queryParams.sort_by = this._getSortValue(); 6010 6011 if (self.queryParams.page) { 6012 delete self.queryParams.page; 6013 } 6014 window.location.search = decodeURIComponent($.param(self.queryParams)); 6015 }, 6016 6017 _onFilterChange: function() { 6018 document.location.href = this._getFilterValue(); 6019 }, 6020 6021 _getFilterValue: function() { 6022 return this.$filterSelect.val(); 6023 }, 6024 6025 _getSortValue: function() { 6026 return this.$sortSelect.val() || this.defaultSort; 6027 }, 6028 6029 _getDefaultSortValue: function() { 6030 return this.$sortSelect.attr(data.sortBy); 6031 }, 6032 6033 onUnload: function() { 6034 this.$filterSelect.off('change', this._onFilterChange); 6035 this.$sortSelect.off('change', this._onSortChange); 6036 } 6037 }); 6038 6039 return Filters; 6040})(); 6041 6042window.theme = window.theme || {}; 6043 6044theme.HeaderSection = (function() { 6045 function Header() { 6046 theme.Header.init(); 6047 theme.MobileNav.init(); 6048 theme.SearchDrawer.init(); 6049 theme.Search.init(); 6050 } 6051 6052 Header.prototype = _.assignIn({}, Header.prototype, { 6053 onUnload: function() { 6054 theme.Header.unload(); 6055 theme.Search.unload(); 6056 } 6057 }); 6058 6059 return Header; 6060})(); 6061 6062theme.Maps = (function() { 6063 var config = { 6064 zoom: 14 6065 }; 6066 var apiStatus = null; 6067 var mapsToLoad = []; 6068 6069 var errors = { 6070 addressNoResults: theme.strings.addressNoResults, 6071 addressQueryLimit: theme.strings.addressQueryLimit, 6072 addressError: theme.strings.addressError, 6073 authError: theme.strings.authError 6074 }; 6075 6076 var selectors = { 6077 section: '[data-section-type="map"]', 6078 map: '[data-map]', 6079 mapOverlay: '[data-map-overlay]' 6080 }; 6081 6082 var classes = { 6083 mapError: 'map-section--load-error', 6084 errorMsg: 'map-section__error errors text-center' 6085 }; 6086 6087 // Global function called by Google on auth errors. 6088 // Show an auto error message on all map instances. 6089 // eslint-disable-next-line camelcase, no-unused-vars 6090 window.gm_authFailure = function() { 6091 if (!Shopify.designMode) { 6092 return; 6093 } 6094
6095 $(selectors.section).addClass(classes.mapError); 6096 $(selectors.map).remove(); 6097 $(selectors.mapOverlay).after( 6098 '<div class="' + 6099 classes.errorMsg + 6100 '">' + 6101 theme.strings.authError + 6102 '</div>' 6103 ); 6104 }; 6105 6106 function Map(container) { 6107 this.$container = $(container); 6108 this.$map = this.$container.find(selectors.map); 6109 this.key = this.$map.data('api-key'); 6110 6111 if (typeof this.key === 'undefined') { 6112 return; 6113 } 6114 6115 if (apiStatus === 'loaded') { 6116 this.createMap(); 6117 } else { 6118 mapsToLoad.push(this); 6119 6120 if (apiStatus !== 'loading') { 6121 apiStatus = 'loading'; 6122 if (typeof window.google === 'undefined') { 6123 $.getScript( 6124 'https://maps.googleapis.com/maps/api/js?key=' + this.key 6125 ).then(function() { 6126 apiStatus = 'loaded'; 6127 initAllMaps(); 6128 }); 6129 } 6130 } 6131 } 6132 } 6133 6134 function initAllMaps() { 6135 // API has loaded, load all Map instances in queue 6136 $.each(mapsToLoad, function(index, instance) { 6137 instance.createMap(); 6138 }); 6139 } 6140 6141 function geolocate($map) { 6142 var deferred = $.Deferred(); 6143 var geocoder = new google.maps.Geocoder(); 6144 var address = $map.data('address-setting'); 6145 6146 geocoder.geocode({ address: address }, function(results, status) { 6147 if (status !== google.maps.GeocoderStatus.OK) { 6148 deferred.reject(status); 6149 } 6150 6151 deferred.resolve(results); 6152 }); 6153 6154 return deferred; 6155 } 6156 6157 Map.prototype = _.assignIn({}, Map.prototype, { 6158 createMap: function() { 6159 var $map = this.$map; 6160 6161 return geolocate($map) 6162 .then( 6163 function(results) { 6164 var mapOptions = { 6165 zoom: config.zoom, 6166 center: results[0].geometry.location, 6167 draggable: false, 6168 clickableIcons: false, 6169 scrollwheel: false, 6170 disableDoubleClickZoom: true, 6171 disableDefaultUI: true 6172 }; 6173 6174 var map = (this.map = new google.maps.Map($map[0], mapOptions)); 6175 var center = (this.center = map.getCenter()); 6176 6177 //eslint-disable-next-line no-unused-vars 6178 var marker = new google.maps.Marker({ 6179 map: map, 6180 position: map.getCenter() 6181 }); 6182 6183 google.maps.event.addDomListener( 6184 window, 6185 'resize', 6186 $.debounce(250, function() { 6187 google.maps.event.trigger(map, 'resize'); 6188 map.setCenter(center); 6189 $map.removeAttr('style'); 6190 }) 6191 ); 6192 }.bind(this) 6193 ) 6194 .fail(function() { 6195 var errorMessage; 6196 6197 switch (status) { 6198 case 'ZERO_RESULTS': 6199 errorMessage = errors.addressNoResults; 6200 break; 6201 case 'OVER_QUERY_LIMIT': 6202 errorMessage = errors.addressQueryLimit; 6203 break; 6204 case 'REQUEST_DENIED': 6205 errorMessage = errors.authError; 6206 break; 6207 default: 6208 errorMessage = errors.addressError; 6209 break; 6210 } 6211 6212 // Show errors only to merchant in the editor. 6213 if (Shopify.designMode) { 6214 $map 6215 .parent()
6216 .addClass(classes.mapError) 6217 .html( 6218 '<div class="' + 6219 classes.errorMsg + 6220 '">' + 6221 errorMessage + 6222 '</div>' 6223 ); 6224 } 6225 }); 6226 }, 6227 6228 onUnload: function() { 6229 if (this.$map.length === 0) { 6230 return; 6231 } 6232 google.maps.event.clearListeners(this.map, 'resize'); 6233 } 6234 }); 6235 6236 return Map; 6237})(); 6238 6239/* eslint-disable no-new */ 6240theme.Product = (function() { 6241 function Product(container) { 6242 var $container = (this.$container = $(container)); 6243 var sectionId = $container.attr('data-section-id'); 6244 this.ajaxEnabled = $container.data('ajax-enabled'); 6245 6246 this.settings = { 6247 // Breakpoints from src/stylesheets/global/variables.scss.liquid 6248 mediaQueryMediumUp: 'screen and (min-width: 750px)', 6249 mediaQuerySmall: 'screen and (max-width: 749px)', 6250 bpSmall: false, 6251 enableHistoryState: $container.data('enable-history-state') || false, 6252 namespace: '.slideshow-' + sectionId, 6253 sectionId: sectionId, 6254 sliderActive: false, 6255 zoomEnabled: false 6256 }; 6257 6258 this.selectors = { 6259 addToCart: '[data-add-to-cart]', 6260 addToCartText: '[data-add-to-cart-text]', 6261 cartCount: '[data-cart-count]', 6262 cartCountBubble: '[data-cart-count-bubble]', 6263 cartPopup: '[data-cart-popup]', 6264 cartPopupCartQuantity: '[data-cart-popup-cart-quantity]', 6265 cartPopupClose: '[data-cart-popup-close]', 6266 cartPopupDismiss: '[data-cart-popup-dismiss]', 6267 cartPopupImage: '[data-cart-popup-image]', 6268 cartPopupImageWrapper: '[data-cart-popup-image-wrapper]', 6269 cartPopupImagePlaceholder: '[data-cart-popup-image-placeholder]', 6270 cartPopupPlaceholderSize: '[data-placeholder-size]', 6271 cartPopupProductDetails: '[data-cart-popup-product-details]', 6272 cartPopupQuantity: '[data-cart-popup-quantity]', 6273 cartPopupQuantityLabel: '[data-cart-popup-quantity-label]', 6274 cartPopupTitle: '[data-cart-popup-title]', 6275 cartPopupWrapper: '[data-cart-popup-wrapper]', 6276 loader: '[data-loader]', 6277 loaderStatus: '[data-loader-status]', 6278 quantity: '[data-quantity-input]', 6279 SKU: '.variant-sku', 6280 productStatus: '[data-product-status]', 6281 originalSelectorId: '#ProductSelect-' + sectionId, 6282 productForm: '[data-product-form]', 6283 errorMessage: '[data-error-message]', 6284 errorMessageWrapper: '[data-error-message-wrapper]', 6285 imageZoomWrapper: '[data-image-zoom-wrapper]', 6286 productMediaWrapper: '[data-product-single-media-wrapper]', 6287 productThumbImages: '.product-single__thumbnail--' + sectionId, 6288 productThumbs: '.product-single__thumbnails-' + sectionId, 6289 productThumbListItem: '.product-single__thumbnails-item', 6290 productThumbsWrapper: '.thumbnails-wrapper', 6291 saleLabel: '.product-price__sale-label-' + sectionId, 6292 singleOptionSelector: '.single-option-selector-' + sectionId, 6293 shopifyPaymentButton: '.shopify-payment-button', 6294 productMediaTypeVideo: '[data-product-media-type-video]', 6295 productMediaTypeModel: '[data-product-media-type-model]', 6296 priceContainer: '[data-price]', 6297 regularPrice: '[data-regular-price]', 6298 salePrice: '[data-sale-price]', 6299 unitPrice: '[data-unit-price]', 6300 unitPriceBaseUnit: '[data-unit-price-base-unit]', 6301 productPolicies: '[data-product-policies]' 6302 }; 6303 6304 this.classes = { 6305 cartPopupWrapperHidden: 'cart-popup-wrapper--hidden', 6306 hidden: 'hide', 6307 visibilityHidden: 'visibility-hidden', 6308 inputError: 'input--error', 6309 jsZoomEnabled: 'js-zoom-enabled', 6310 productOnSale: 'price--on-sale', 6311 productUnitAvailable: 'price--unit-available', 6312 productUnavailable: 'price--unavailable', 6313 productSoldOut: 'price--sold-out', 6314 cartImage: 'cart-popup-item__image', 6315 productFormErrorMessageWrapperHidden: 6316 'product-form__error-message-wrapper--hidden', 6317 activeClass: 'active-thumb', 6318 variantSoldOut: 'product-form--variant-sold-out' 6319 }; 6320 6321 this.$quantityInput = $(this.selectors.quantity, $container); 6322 this.$errorMessageWrapper = $( 6323 this.selectors.errorMessageWrapper, 6324 $container 6325 ); 6326 this.$addToCart = $(this.selectors.addToCart, $container); 6327 this.$addToCartText = $(this.selectors.addToCartText, this.$addToCart); 6328 this.$shopifyPaymentButton = $( 6329 this.selectors.shopifyPaymentButton, 6330 $container 6331 ); 6332 this.$productPolicies = $(this.selectors.productPolicies, $container); 6333 6334 this.$loader = $(this.selectors.loader, this.$addToCart); 6335 this.$loaderStatus = $(this.selectors.loaderStatus, $container); 6336 6337 // Stop parsing if we don't have the product json script tag when loading 6338 // section in the Theme Editor 6339 if (!$('#ProductJson-' + sectionId).html()) { 6340 return; 6341 } 6342 6343 this.productSingleObject = JSON.parse( 6344 document.getElementById('ProductJson-' + sectionId).innerHTML 6345 ); 6346 6347 this.settings.zoomEnabled = $(this.selectors.imageZoomWrapper).hasClass(
6348 this.classes.jsZoomEnabled 6349 ); 6350 6351 this._initBreakpoints(); 6352 this._stringOverrides(); 6353 this._initVariants(); 6354 this._initMediaSwitch(); 6355 this._initAddToCart(); 6356 this._setActiveThumbnail(); 6357 this._initProductVideo(); 6358 this._initModelViewerLibraries(); 6359 this._initShopifyXrLaunch(); 6360 } 6361 6362 Product.prototype = _.assignIn({}, Product.prototype, { 6363 _stringOverrides: function() { 6364 theme.productStrings = theme.productStrings || {}; 6365 $.extend(theme.strings, theme.productStrings); 6366 }, 6367 6368 _initBreakpoints: function() { 6369 var self = this; 6370 6371 enquire.register(this.settings.mediaQuerySmall, { 6372 match: function() { 6373 // initialize thumbnail slider on mobile if more than four thumbnails 6374 if ($(self.selectors.productThumbImages).length > 4) { 6375 self._initThumbnailSlider(); 6376 } 6377 6378 // destroy image zooming if enabled 6379 if (self.settings.zoomEnabled) { 6380 $(self.selectors.imageZoomWrapper).each(function() { 6381 _destroyZoom(this); 6382 }); 6383 } 6384 6385 self.settings.bpSmall = true; 6386 }, 6387 unmatch: function() { 6388 if (self.settings.sliderActive) { 6389 self._destroyThumbnailSlider(); 6390 } 6391 6392 self.settings.bpSmall = false; 6393 } 6394 }); 6395 6396 enquire.register(this.settings.mediaQueryMediumUp, { 6397 match: function() { 6398 if (self.settings.zoomEnabled) { 6399 $(self.selectors.imageZoomWrapper).each(function() { 6400 _enableZoom(this); 6401 }); 6402 } 6403 } 6404 }); 6405 }, 6406 6407 _initVariants: function() { 6408 var options = { 6409 $container: this.$container, 6410 enableHistoryState: 6411 this.$container.data('enable-history-state') || false, 6412 singleOptionSelector: this.selectors.singleOptionSelector, 6413 originalSelectorId: this.selectors.originalSelectorId, 6414 product: this.productSingleObject 6415 }; 6416 6417 this.variants = new slate.Variants(options); 6418 6419 this.$container.on( 6420 'variantChange' + this.settings.namespace, 6421 this._updateAvailability.bind(this) 6422 ); 6423 this.$container.on( 6424 'variantImageChange' + this.settings.namespace, 6425 this._updateMedia.bind(this) 6426 ); 6427 this.$container.on( 6428 'variantPriceChange' + this.settings.namespace, 6429 this._updatePrice.bind(this) 6430 ); 6431 this.$container.on( 6432 'variantSKUChange' + this.settings.namespace, 6433 this._updateSKU.bind(this) 6434 ); 6435 }, 6436 6437 _initMediaSwitch: function() { 6438 if (!$(this.selectors.productThumbImages).length) { 6439 return; 6440 } 6441 6442 var self = this; 6443 6444 $(this.selectors.productThumbImages) 6445 .on('click', function(evt) { 6446 evt.preventDefault(); 6447 var $el = $(this); 6448 6449 var mediaId = $el.data('thumbnail-id'); 6450 6451 self._switchMedia(mediaId); 6452 self._setActiveThumbnail(mediaId); 6453 }) 6454 .on('keyup', self._handleMediaFocus.bind(self)); 6455 }, 6456 6457 _initAddToCart: function() { 6458 $(this.selectors.productForm, this.$container).on( 6459 'submit', 6460 function(evt) { 6461 if (this.$addToCart.is('[aria-disabled]')) { 6462 evt.preventDefault(); 6463 return; 6464 } 6465 6466 if (!this.ajaxEnabled) return; 6467 6468 evt.preventDefault(); 6469 6470 this.$previouslyFocusedElement = $(':focus'); 6471 6472 var isInvalidQuantity = this.$quantityInput.val() <= 0; 6473 6474 if (isInvalidQuantity) { 6475 this._showErrorMessage(theme.strings.quantityMinimumMessage); 6476 return; 6477 } 6478 6479 if (!isInvalidQuantity && this.ajaxEnabled) { 6480 // disable the addToCart and dynamic checkout button while 6481 // request/cart popup is loading and handle loading state 6482 this._handleButtonLoadingState(true); 6483 var $data = $(this.selectors.productForm, this.$container); 6484 this._addItemToCart($data); 6485 return; 6486 } 6487 }.bind(this) 6488 ); 6489 }, 6490 6491 _initProductVideo: function() { 6492 var sectionId = this.settings.sectionId; 6493 6494 $(this.selectors.productMediaTypeVideo, this.$container).each(function() { 6495 var $el = $(this); 6496 theme.ProductVideo.init($el, sectionId); 6497 }); 6498 }, 6499 6500 _initModelViewerLibraries: function() { 6501 var $modelViewerElements = $( 6502 this.selectors.productMediaTypeModel, 6503 this.$container 6504 ); 6505 if ($modelViewerElements.length < 1) return; 6506 theme.ProductModel.init($modelViewerElements, this.settings.sectionId); 6507 }, 6508 6509 _initShopifyXrLaunch: function() { 6510 var self = this; 6511 $(document).on('shopify_xr_launch', function() { 6512 var $currentMedia = $( 6513 self.selectors.productMediaWrapper + 6514 ':not(.' + 6515 self.classes.hidden + 6516 ')', 6517 self.$container 6518 ); 6519 $currentMedia.trigger('xrLaunch'); 6520 }); 6521 }, 6522 6523 _addItemToCart: function(data) { 6524 var params = { 6525 url: '/cart/add.js', 6526 data: $(data).serialize(), 6527 dataType: 'json' 6528 }; 6529 6530 $.post(params) 6531 .done( 6532 function(item) { 6533 this._hideErrorMessage(); 6534 this._setupCartPopup(item); 6535 }.bind(this) 6536 ) 6537 .fail( 6538 function(response) { 6539 this.$previouslyFocusedElement.focus(); 6540 var errorMessage = response.responseJSON 6541 ? response.responseJSON.description 6542 : theme.strings.cartError; 6543 this._showErrorMessage(errorMessage); 6544 this._handleButtonLoadingState(false); 6545 }.bind(this) 6546 ); 6547 }, 6548 6549 _handleButtonLoadingState: function(isLoading) { 6550 if (isLoading) { 6551 this.$addToCart.attr('aria-disabled', true); 6552 this.$addToCartText.addClass(this.classes.hidden); 6553 this.$loader.removeClass(this.classes.hidden); 6554 this.$shopifyPaymentButton.attr('disabled', true); 6555 this.$loaderStatus.attr('aria-hidden', false); 6556 } else { 6557 this.$addToCart.removeAttr('aria-disabled'); 6558 this.$addToCartText.removeClass(this.classes.hidden); 6559 this.$loader.addClass(this.classes.hidden); 6560 this.$shopifyPaymentButton.removeAttr('disabled'); 6561 this.$loaderStatus.attr('aria-hidden', true); 6562 } 6563 }, 6564 6565 _showErrorMessage: function(errorMessage) { 6566 $(this.selectors.errorMessage, this.$container).html(errorMessage); 6567 6568 if (this.$quantityInput.length !== 0) { 6569 this.$quantityInput.addClass(this.classes.inputError); 6570 } 6571 6572 this.$errorMessageWrapper 6573 .removeClass(this.classes.productFormErrorMessageWrapperHidden) 6574 .attr('aria-hidden', true) 6575 .removeAttr('aria-hidden'); 6576 }, 6577 6578 _hideErrorMessage: function() { 6579 this.$errorMessageWrapper.addClass( 6580 this.classes.productFormErrorMessageWrapperHidden 6581 ); 6582 6583 if (this.$quantityInput.length !== 0) { 6584 this.$quantityInput.removeClass(this.classes.inputError); 6585 } 6586 }, 6587 6588 _setupCartPopup: function(item) { 6589 this.$cartPopup = this.$cartPopup || $(this.selectors.cartPopup); 6590 this.$cartPopupWrapper = 6591 this.$cartPopupWrapper || $(this.selectors.cartPopupWrapper); 6592 this.$cartPopupTitle = 6593 this.$cartPopupTitle || $(this.selectors.cartPopupTitle); 6594 this.$cartPopupQuantity = 6595 this.$cartPopupQuantity || $(this.selectors.cartPopupQuantity); 6596 this.$cartPopupQuantityLabel = 6597 this.$cartPopupQuantityLabel || 6598 $(this.selectors.cartPopupQuantityLabel); 6599 this.$cartPopupClose = 6600 this.$cartPopupClose || $(this.selectors.cartPopupClose); 6601 this.$cartPopupDismiss = 6602 this.$cartPopupDismiss || $(this.selectors.cartPopupDismiss); 6603 this.$cartPopupImagePlaceholder = 6604 this.$cartPopupImagePlaceholder || 6605 $(this.selectors.cartPopupImagePlaceholder); 6606 6607 this._setupCartPopupEventListeners(); 6608 6609 this._updateCartPopupContent(item); 6610 }, 6611 6612 _updateCartPopupContent: function(item) { 6613 var quantity = this.$quantityInput.length ? this.$quantityInput.val() : 1; 6614 6615 this.$cartPopupTitle.text(item.product_title); 6616 this.$cartPopupQuantity.text(quantity); 6617 this.$cartPopupQuantityLabel.text( 6618 theme.strings.quantityLabel.replace('[count]', quantity) 6619 ); 6620 6621 this._setCartPopupPlaceholder( 6622 item.featured_image.url,
6623 item.featured_image.aspect_ratio 6624 ); 6625 this._setCartPopupImage(item.featured_image.url, item.featured_image.alt); 6626 this._setCartPopupProductDetails( 6627 item.product_has_only_default_variant, 6628 item.options_with_values, 6629 item.properties 6630 ); 6631 6632 $.getJSON('/cart.js').then( 6633 function(cart) { 6634 this._setCartQuantity(cart.item_count); 6635 this._setCartCountBubble(cart.item_count); 6636 this._showCartPopup(); 6637 }.bind(this) 6638 ); 6639 }, 6640 6641 _setupCartPopupEventListeners: function() { 6642 this.$cartPopupWrapper.on( 6643 'keyup', 6644 function(event) { 6645 if (event.keyCode === slate.utils.keyboardKeys.ESCAPE) { 6646 this._hideCartPopup(event); 6647 } 6648 }.bind(this) 6649 ); 6650 6651 this.$cartPopupClose.on('click', this._hideCartPopup.bind(this)); 6652 this.$cartPopupDismiss.on('click', this._hideCartPopup.bind(this)); 6653 $('body').on('click', this._onBodyClick.bind(this)); 6654 }, 6655 6656 _setCartPopupPlaceholder: function(imageUrl, imageAspectRatio) { 6657 this.$cartPopupImageWrapper = 6658 this.$cartPopupImageWrapper || $(this.selectors.cartPopupImageWrapper); 6659 6660 if (imageUrl === null) { 6661 this.$cartPopupImageWrapper.addClass(this.classes.hidden); 6662 return; 6663 } 6664 6665 var $placeholder = $(this.selectors.cartPopupPlaceholderSize); 6666 var maxWidth = 95 * imageAspectRatio; 6667 var heightRatio = 100 / imageAspectRatio; 6668 6669 this.$cartPopupImagePlaceholder.css('max-width', maxWidth); 6670 6671 $placeholder.css('padding-top', heightRatio + '%'); 6672 }, 6673 6674 _setCartPopupImage: function(imageUrl, imageAlt) { 6675 if (imageUrl === null) return; 6676 6677 this.$cartPopupImageWrapper.removeClass(this.classes.hidden); 6678 var sizedImageUrl = theme.Images.getSizedImageUrl(imageUrl, '200x'); 6679 var image = document.createElement('img'); 6680 image.src = sizedImageUrl; 6681 image.alt = imageAlt; 6682 image.classList.add(this.classes.cartImage); 6683 image.dataset.cartPopupImage = ''; 6684 6685 image.onload = function() { 6686 this.$cartPopupImagePlaceholder.addClass(this.classes.hidden); 6687 this.$cartPopupImageWrapper.append(image); 6688 }.bind(this); 6689 }, 6690 6691 _setCartPopupProductDetails: function( 6692 product_has_only_default_variant, 6693 options, 6694 properties 6695 ) { 6696 this.$cartPopupProductDetails = 6697 this.$cartPopupProductDetails || 6698 $(this.selectors.cartPopupProductDetails); 6699 var variantPropertiesHTML = ''; 6700 6701 if (!product_has_only_default_variant) { 6702 variantPropertiesHTML = 6703 variantPropertiesHTML + this._getVariantOptionList(options); 6704 } 6705 6706 if (properties !== null && Object.keys(properties).length !== 0) { 6707 variantPropertiesHTML = 6708 variantPropertiesHTML + this._getPropertyList(properties); 6709 } 6710 6711 if (variantPropertiesHTML.length === 0) { 6712 this.$cartPopupProductDetails.html(''); 6713 this.$cartPopupProductDetails.attr('hidden', ''); 6714 } else { 6715 this.$cartPopupProductDetails.html(variantPropertiesHTML); 6716 this.$cartPopupProductDetails.removeAttr('hidden'); 6717 } 6718 }, 6719 6720 _getVariantOptionList: function(variantOptions) { 6721 var variantOptionListHTML = ''; 6722 6723 variantOptions.forEach(function(variantOption) { 6724 variantOptionListHTML = 6725 variantOptionListHTML + 6726 '<li class="product-details__item product-details__item--variant-option">' + 6727 variantOption.name + 6728 ': ' + 6729 variantOption.value + 6730 '</li>'; 6731 }); 6732 6733 return variantOptionListHTML; 6734 }, 6735 6736 _getPropertyList: function(properties) { 6737 var propertyListHTML = ''; 6738 var propertiesArray = Object.entries(properties); 6739 6740 propertiesArray.forEach(function(property) { 6741 // Line item properties prefixed with an underscore are not to be displayed 6742 if (property[0].charAt(0) === '_') return; 6743 6744 // if the property value has a length of 0 (empty), don't display it 6745 if (property[1].length === 0) return; 6746 6747 propertyListHTML = 6748 propertyListHTML + 6749 '<li class="product-details__item product-details__item--property">' + 6750 '<span class="product-details__property-label">' + 6751 property[0] + 6752 ': </span>' + 6753 property[1]; 6754 ': ' + '</li>'; 6755 }); 6756 6757 return propertyListHTML; 6758 }, 6759 6760 _setCartQuantity: function(quantity) { 6761 this.$cartPopupCartQuantity = 6762 this.$cartPopupCartQuantity || $(this.selectors.cartPopupCartQuantity); 6763 var ariaLabel; 6764 6765 if (quantity === 1) { 6766 ariaLabel = theme.strings.oneCartCount; 6767 } else if (quantity > 1) { 6768 ariaLabel = theme.strings.otherCartCount.replace('[count]', quantity); 6769 } 6770 6771 this.$cartPopupCartQuantity.text(quantity).attr('aria-label', ariaLabel); 6772 }, 6773 6774 _setCartCountBubble: function(quantity) { 6775 this.$cartCountBubble = 6776 this.$cartCountBubble || $(this.selectors.cartCountBubble); 6777 this.$cartCount = this.$cartCount || $(this.selectors.cartCount); 6778 6779 this.$cartCountBubble.removeClass(this.classes.hidden); 6780 this.$cartCount.text(quantity); 6781 }, 6782 6783 _showCartPopup: function() { 6784 this.$cartPopupWrapper 6785 .prepareTransition() 6786 .removeClass(this.classes.cartPopupWrapperHidden); 6787 this._handleButtonLoadingState(false); 6788 6789 slate.a11y.trapFocus({ 6790 $container: this.$cartPopupWrapper, 6791 $elementToFocus: this.$cartPopup, 6792 namespace: 'cartPopupFocus' 6793 }); 6794 }, 6795 6796 _hideCartPopup: function(event) { 6797 var setFocus = event.detail === 0 ? true : false;
6798 this.$cartPopupWrapper 6799 .prepareTransition() 6800 .addClass(this.classes.cartPopupWrapperHidden); 6801 6802 $(this.selectors.cartPopupImage).remove(); 6803 this.$cartPopupImagePlaceholder.removeClass(this.classes.hidden); 6804 6805 slate.a11y.removeTrapFocus({ 6806 $container: this.$cartPopupWrapper, 6807 namespace: 'cartPopupFocus' 6808 }); 6809 6810 if (setFocus) this.$previouslyFocusedElement[0].focus(); 6811 6812 this.$cartPopupWrapper.off('keyup'); 6813 this.$cartPopupClose.off('click'); 6814 this.$cartPopupDismiss.off('click'); 6815 $('body').off('click'); 6816 }, 6817 6818 _onBodyClick: function(event) { 6819 var $target = $(event.target); 6820 6821 if ( 6822 $target[0] !== this.$cartPopupWrapper[0] && 6823 !$target.parents(this.selectors.cartPopup).length 6824 ) { 6825 this._hideCartPopup(event); 6826 } 6827 }, 6828 6829 _setActiveThumbnail: function(mediaId) { 6830 // If there is no element passed, find it by the current product image 6831 if (typeof mediaId === 'undefined') { 6832 mediaId = $( 6833 this.selectors.productMediaWrapper + ':not(.hide)', 6834 this.$container 6835 ).data('media-id'); 6836 } 6837 6838 var $thumbnailWrappers = $( 6839 this.selectors.productThumbListItem + ':not(.slick-cloned)', 6840 this.$container 6841 ); 6842 6843 var $activeThumbnail = $thumbnailWrappers.find( 6844 this.selectors.productThumbImages + 6845 "[data-thumbnail-id='" + 6846 mediaId + 6847 "']" 6848 ); 6849 6850 $(this.selectors.productThumbImages) 6851 .removeClass(this.classes.activeClass) 6852 .removeAttr('aria-current'); 6853 6854 $activeThumbnail.addClass(this.classes.activeClass); 6855 $activeThumbnail.attr('aria-current', true); 6856 6857 if (!$thumbnailWrappers.hasClass('slick-slide')) { 6858 return; 6859 } 6860 6861 var slideIndex = $activeThumbnail.parent().data('slick-index'); 6862 6863 $(this.selectors.productThumbs).slick('slickGoTo', slideIndex, true); 6864 }, 6865 6866 _switchMedia: function(mediaId) { 6867 var $currentMedia = $( 6868 this.selectors.productMediaWrapper + 6869 ':not(.' + 6870 this.classes.hidden + 6871 ')', 6872 this.$container 6873 ); 6874 6875 var $newMedia = $( 6876 this.selectors.productMediaWrapper + 6877 "[data-media-id='" + 6878 mediaId + 6879 "']", 6880 this.$container 6881 ); 6882 6883 var $otherMedia = $( 6884 this.selectors.productMediaWrapper + 6885 ":not([data-media-id='" + 6886 mediaId + 6887 "'])", 6888 this.$container 6889 ); 6890 6891 $currentMedia.trigger('mediaHidden'); 6892 $newMedia.removeClass(this.classes.hidden).trigger('mediaVisible'); 6893 $otherMedia.addClass(this.classes.hidden); 6894 }, 6895 6896 _handleMediaFocus: function(evt) { 6897 if (evt.keyCode !== slate.utils.keyboardKeys.ENTER) return; 6898 6899 var mediaId = $(evt.currentTarget).data('thumbnail-id'); 6900 6901 $( 6902 this.selectors.productMediaWrapper + 6903 "[data-media-id='" + 6904 mediaId + 6905 "']", 6906 this.$container 6907 ).focus(); 6908 }, 6909 6910 _initThumbnailSlider: function() { 6911 var options = { 6912 slidesToShow: 3, 6913 slidesToScroll: 2, 6914 infinite: false, 6915 prevArrow: '.thumbnails-slider__prev--' + this.settings.sectionId,
6916 nextArrow: '.thumbnails-slider__next--' + this.settings.sectionId 6917 }; 6918 6919 $(this.selectors.productThumbs).slick(options); 6920 6921 // Accessibility concerns not yet fixed in Slick Slider 6922 $(this.selectors.productThumbsWrapper, this.$container) 6923 .find('.slick-list') 6924 .removeAttr('aria-live'); 6925 $(this.selectors.productThumbsWrapper, this.$container) 6926 .find('.slick-disabled') 6927 .removeAttr('aria-disabled'); 6928 6929 this.settings.sliderActive = true; 6930 }, 6931 6932 _destroyThumbnailSlider: function() { 6933 $(this.selectors.productThumbs).slick('unslick'); 6934 this.settings.sliderActive = false; 6935 6936 // Accessibility concerns not yet fixed in Slick Slider 6937 $(this.selectors.productThumbsWrapper, this.$container) 6938 .find('[tabindex="-1"]') 6939 .removeAttr('tabindex'); 6940 }, 6941 6942 _liveRegionText: function(variant) { 6943 // Dummy content for live region 6944 var liveRegionText = 6945 '[Availability] [Regular] [$$] [Sale] [$]. [UnitPrice] [$$$]'; 6946 6947 if (!variant) { 6948 liveRegionText = theme.strings.unavailable; 6949 return liveRegionText; 6950 } 6951 6952 // Update availability 6953 var availability = variant.available ? '' : theme.strings.soldOut + ','; 6954 liveRegionText = liveRegionText.replace('[Availability]', availability); 6955 6956 // Update pricing information 6957 var regularLabel = ''; 6958 var regularPrice = theme.Currency.formatMoney( 6959 variant.price, 6960 theme.moneyFormat 6961 ); 6962 var saleLabel = ''; 6963 var salePrice = ''; 6964 var unitLabel = ''; 6965 var unitPrice = ''; 6966 6967 if (variant.compare_at_price > variant.price) { 6968 regularLabel = theme.strings.regularPrice; 6969 regularPrice = 6970 theme.Currency.formatMoney( 6971 variant.compare_at_price, 6972 theme.moneyFormat 6973 ) + ','; 6974 saleLabel = theme.strings.sale; 6975 salePrice = theme.Currency.formatMoney( 6976 variant.price, 6977 theme.moneyFormat 6978 ); 6979 } 6980 6981 if (variant.unit_price) { 6982 unitLabel = theme.strings.unitPrice; 6983 unitPrice = 6984 theme.Currency.formatMoney(variant.unit_price, theme.moneyFormat) + 6985 ' ' + 6986 theme.strings.unitPriceSeparator + 6987 ' ' + 6988 this._getBaseUnit(variant); 6989 } 6990 6991 liveRegionText = liveRegionText 6992 .replace('[Regular]', regularLabel) 6993 .replace('[$$]', regularPrice) 6994 .replace('[Sale]', saleLabel) 6995 .replace('[$]', salePrice) 6996 .replace('[UnitPrice]', unitLabel) 6997 .replace('[$$$]', unitPrice) 6998 .trim(); 6999 7000 return liveRegionText; 7001 }, 7002 7003 _updateLiveRegion: function(evt) { 7004 var variant = evt.variant; 7005 var liveRegion = this.container.querySelector( 7006 this.selectors.productStatus 7007 ); 7008 liveRegion.innerHTML = this._liveRegionText(variant); 7009 liveRegion.setAttribute('aria-hidden', false); 7010 7011 // hide content from accessibility tree after announcement 7012 setTimeout(function() { 7013 liveRegion.setAttribute('aria-hidden', true); 7014 }, 1000); 7015 }, 7016 7017 _updateAddToCart: function(evt) { 7018 var variant = evt.variant; 7019 7020 if (variant) { 7021 if (variant.available) { 7022 this.$addToCart 7023 .removeAttr('aria-disabled') 7024 .attr('aria-label', theme.strings.addToCart); 7025 $(this.selectors.addToCartText, this.$container).text( 7026 theme.strings.addToCart 7027 ); 7028 $(this.selectors.productForm, this.container).removeClass( 7029 this.classes.variantSoldOut 7030 ); 7031 } else { 7032 // Variant is sold out, disable submit button and change the text. 7033 this.$addToCart 7034 .attr('aria-disabled', true) 7035 .attr('aria-label', theme.strings.soldOut); 7036 $(this.selectors.addToCartText, this.$container).text( 7037 theme.strings.soldOut 7038 ); 7039 $(this.selectors.productForm, this.container).addClass( 7040 this.classes.variantSoldOut 7041 ); 7042 } 7043 } else { 7044 // The variant doesn't exist, disable submit button and change the text. 7045 this.$addToCart 7046 .attr('aria-disabled', true) 7047 .attr('aria-label', theme.strings.unavailable); 7048 $(this.selectors.addToCartText, this.$container).text( 7049 theme.strings.unavailable 7050 ); 7051 $(this.selectors.productForm, this.container).addClass( 7052 this.classes.variantSoldOut 7053 ); 7054 } 7055 }, 7056 7057 _updateAvailability: function(evt) { 7058 // remove error message if one is showing 7059 this._hideErrorMessage(); 7060 7061 // update form submit 7062 this._updateAddToCart(evt); 7063 // update live region 7064 this._updateLiveRegion(evt); 7065 7066 this._updatePrice(evt); 7067 }, 7068 7069 _updateMedia: function(evt) { 7070 var variant = evt.variant; 7071 var mediaId = variant.featured_media.id; 7072 var sectionMediaId = this.settings.sectionId + '-' + mediaId; 7073 7074 this._switchMedia(sectionMediaId);
7075 this._setActiveThumbnail(sectionMediaId); 7076 }, 7077 7078 _updatePrice: function(evt) { 7079 var variant = evt.variant; 7080 7081 var $priceContainer = $(this.selectors.priceContainer, this.$container); 7082 var $regularPrice = $(this.selectors.regularPrice, $priceContainer); 7083 var $salePrice = $(this.selectors.salePrice, $priceContainer); 7084 var $unitPrice = $(this.selectors.unitPrice, $priceContainer); 7085 var $unitPriceBaseUnit = $( 7086 this.selectors.unitPriceBaseUnit, 7087 $priceContainer 7088 ); 7089 7090 // Reset product price state 7091 $priceContainer 7092 .removeClass(this.classes.productUnavailable) 7093 .removeClass(this.classes.productOnSale) 7094 .removeClass(this.classes.productUnitAvailable) 7095 .removeClass(this.classes.productSoldOut) 7096 .removeAttr('aria-hidden'); 7097 7098 this.$productPolicies.removeClass(this.classes.visibilityHidden); 7099 7100 // Unavailable 7101 if (!variant) { 7102 $priceContainer 7103 .addClass(this.classes.productUnavailable) 7104 .attr('aria-hidden', true); 7105 7106 this.$productPolicies.addClass(this.classes.visibilityHidden); 7107 return; 7108 } 7109 7110 // Sold out 7111 if (!variant.available) { 7112 $priceContainer.addClass(this.classes.productSoldOut); 7113 } 7114 7115 // On sale 7116 if (variant.compare_at_price > variant.price) { 7117 $regularPrice.html( 7118 theme.Currency.formatMoney( 7119 variant.compare_at_price, 7120 theme.moneyFormat 7121 ) 7122 ); 7123 $salePrice.html( 7124 theme.Currency.formatMoney(variant.price, theme.moneyFormat) 7125 ); 7126 $priceContainer.addClass(this.classes.productOnSale); 7127 } else { 7128 // Regular price 7129 $regularPrice.html( 7130 theme.Currency.formatMoney(variant.price, theme.moneyFormat) 7131 ); 7132 } 7133 7134 // Unit price 7135 if (variant.unit_price) { 7136 $unitPrice.html( 7137 theme.Currency.formatMoney(variant.unit_price, theme.moneyFormat) 7138 ); 7139 $unitPriceBaseUnit.html(this._getBaseUnit(variant)); 7140 $priceContainer.addClass(this.classes.productUnitAvailable); 7141 } 7142 }, 7143 7144 _getBaseUnit: function(variant) { 7145 return variant.unit_price_measurement.reference_value === 1 7146 ? variant.unit_price_measurement.reference_unit 7147 : variant.unit_price_measurement.reference_value + 7148 variant.unit_price_measurement.reference_unit; 7149 }, 7150 7151 _updateSKU: function(evt) { 7152 var variant = evt.variant; 7153 7154 // Update the sku 7155 $(this.selectors.SKU).html(variant.sku); 7156 }, 7157 7158 onUnload: function() { 7159 this.$container.off(this.settings.namespace); 7160 theme.ProductVideo.removeSectionVideos(this.settings.sectionId); 7161 theme.ProductModel.removeSectionModels(this.settings.sectionId); 7162 } 7163 }); 7164 7165 function _enableZoom(el) { 7166 var zoomUrl = $(el).data('zoom'); 7167 $(el).zoom({ 7168 url: zoomUrl 7169 }); 7170 } 7171 7172 function _destroyZoom(el) { 7173 $(el).trigger('zoom.destroy'); 7174 } 7175 7176 return Product; 7177})(); 7178 7179theme.ProductRecommendations = (function() { 7180 function ProductRecommendations(container) { 7181 this.$container = $(container); 7182 7183 var baseUrl = this.$container.data('baseUrl'); 7184 var productId = this.$container.data('productId'); 7185 var recommendationsSectionUrl = 7186 baseUrl + 7187 '?section_id=product-recommendations&product_id=' + 7188 productId + 7189 '&limit=4'; 7190 7191 $.get(recommendationsSectionUrl).then( 7192 function(section) { 7193 var recommendationsMarkup = $(section).html(); 7194 if (recommendationsMarkup.trim() !== '') { 7195 this.$container.html(recommendationsMarkup); 7196 } 7197 }.bind(this) 7198 ); 7199 } 7200 7201 return ProductRecommendations; 7202})(); 7203 7204theme.Quotes = (function() { 7205 var config = { 7206 mediaQuerySmall: 'screen and (max-width: 749px)', 7207 mediaQueryMediumUp: 'screen and (min-width: 750px)', 7208 slideCount: 0 7209 }; 7210 var defaults = { 7211 accessibility: true, 7212 arrows: false, 7213 dots: true, 7214 autoplay: false, 7215 touchThreshold: 20, 7216 slidesToShow: 3, 7217 slidesToScroll: 3 7218 }; 7219 7220 function Quotes(container) { 7221 var $container = (this.$container = $(container)); 7222 var sectionId = $container.attr('data-section-id'); 7223 var wrapper = (this.wrapper = '.quotes-wrapper'); 7224 var slider = (this.slider = '#Quotes-' + sectionId); 7225 var $slider = $(slider, wrapper); 7226 7227 var sliderActive = false;
7228 var mobileOptions = $.extend({}, defaults, { 7229 slidesToShow: 1, 7230 slidesToScroll: 1, 7231 adaptiveHeight: true 7232 }); 7233 7234 config.slideCount = $slider.data('count'); 7235 7236 // Override slidesToShow/Scroll if there are not enough blocks 7237 if (config.slideCount < defaults.slidesToShow) { 7238 defaults.slidesToShow = config.slideCount; 7239 defaults.slidesToScroll = config.slideCount; 7240 } 7241 7242 $slider.on('init', this.a11y.bind(this)); 7243 7244 enquire.register(config.mediaQuerySmall, { 7245 match: function() { 7246 initSlider($slider, mobileOptions); 7247 } 7248 }); 7249 7250 enquire.register(config.mediaQueryMediumUp, { 7251 match: function() { 7252 initSlider($slider, defaults); 7253 } 7254 }); 7255 7256 function initSlider(sliderObj, args) { 7257 if (sliderActive) { 7258 sliderObj.slick('unslick'); 7259 sliderActive = false; 7260 } 7261 7262 sliderObj.slick(args); 7263 sliderActive = true; 7264 } 7265 } 7266 7267 Quotes.prototype = _.assignIn({}, Quotes.prototype, { 7268 onUnload: function() { 7269 enquire.unregister(config.mediaQuerySmall); 7270 enquire.unregister(config.mediaQueryMediumUp); 7271 7272 $(this.slider, this.wrapper).slick('unslick'); 7273 }, 7274 7275 onBlockSelect: function(evt) { 7276 // Ignore the cloned version 7277 var $slide = $( 7278 '.quotes-slide--' + evt.detail.blockId + ':not(.slick-cloned)' 7279 ); 7280 var slideIndex = $slide.data('slick-index'); 7281 7282 // Go to selected slide, pause autoplay 7283 $(this.slider, this.wrapper).slick('slickGoTo', slideIndex); 7284 }, 7285 7286 a11y: function(event, obj) { 7287 var $list = obj.$list; 7288 var $wrapper = $(this.wrapper, this.$container); 7289 7290 // Remove default Slick aria-live attr until slider is focused 7291 $list.removeAttr('aria-live'); 7292 7293 // When an element in the slider is focused set aria-live 7294 $wrapper.on('focusin', function(evt) { 7295 if ($wrapper.has(evt.target).length) { 7296 $list.attr('aria-live', 'polite'); 7297 } 7298 }); 7299 7300 // Remove aria-live 7301 $wrapper.on('focusout', function(evt) { 7302 if ($wrapper.has(evt.target).length) { 7303 $list.removeAttr('aria-live'); 7304 } 7305 }); 7306 } 7307 }); 7308 7309 return Quotes; 7310})(); 7311 7312theme.slideshows = {}; 7313 7314theme.SlideshowSection = (function() { 7315 function SlideshowSection(container) { 7316 var $container = (this.$container = $(container)); 7317 var sectionId = $container.attr('data-section-id'); 7318 var slideshow = (this.slideshow = '#Slideshow-' + sectionId); 7319 7320 theme.slideshows[slideshow] = new theme.Slideshow(slideshow, sectionId); 7321 } 7322 7323 return SlideshowSection; 7324})(); 7325 7326theme.SlideshowSection.prototype = _.assignIn( 7327 {}, 7328 theme.SlideshowSection.prototype, 7329 { 7330 onUnload: function() { 7331 delete theme.slideshows[this.slideshow]; 7332 }, 7333 7334 onBlockSelect: function(evt) { 7335 var $slideshow = $(this.slideshow); 7336 var adaptHeight = $slideshow.data('adapt-height'); 7337 7338 if (adaptHeight) { 7339 theme.slideshows[this.slideshow].setSlideshowHeight(); 7340 } 7341 7342 // Ignore the cloned version 7343 var $slide = $( 7344 '.slideshow__slide--' + evt.detail.blockId + ':not(.slick-cloned)' 7345 ); 7346 var slideIndex = $slide.data('slick-index'); 7347 7348 // Go to selected slide, pause auto-rotate 7349 $slideshow.slick('slickGoTo', slideIndex).slick('slickPause'); 7350 }, 7351 7352 onBlockDeselect: function() { 7353 // Resume auto-rotate 7354 $(this.slideshow).slick('slickPlay'); 7355 } 7356 } 7357); 7358 7359theme.slideshows = {}; 7360 7361theme.VideoSection = (function() { 7362 function VideoSection(container) { 7363 var $container = (this.$container = $(container)); 7364 7365 $('.video', $container).each(function() { 7366 var $el = $(this); 7367 theme.Video.init($el); 7368 theme.Video.editorLoadVideo($el.attr('id')); 7369 }); 7370 } 7371 7372 return VideoSection; 7373})(); 7374 7375theme.VideoSection.prototype = _.assignIn({}, theme.VideoSection.prototype, { 7376 onUnload: function() { 7377 theme.Video.removeEvents(); 7378 } 7379}); 7380 7381theme.heros = {}; 7382 7383theme.HeroSection = (function() { 7384 function HeroSection(container) { 7385 var $container = (this.$container = $(container)); 7386 var sectionId = $container.attr('data-section-id'); 7387 var hero = '#Hero-' + sectionId; 7388 theme.heros[hero] = new theme.Hero(hero, sectionId); 7389 } 7390 7391 return HeroSection; 7392})(); 7393 7394window.theme = window.theme || {}; 7395 7396var selectors = { 7397 disclosureLocale: '[data-disclosure-locale]',
7398 disclosureCurrency: '[data-disclosure-currency]' 7399}; 7400 7401theme.FooterSection = (function() { 7402 function Footer(container) { 7403 this.$container = $(container); 7404 this.cache = {}; 7405 this.cacheSelectors(); 7406 7407 if (this.cache.$localeDisclosure.length) { 7408 this.localeDisclosure = new theme.Disclosure( 7409 this.cache.$localeDisclosure 7410 ); 7411 } 7412 7413 if (this.cache.$currencyDisclosure.length) { 7414 this.currencyDisclosure = new theme.Disclosure( 7415 this.cache.$currencyDisclosure 7416 ); 7417 } 7418 } 7419 7420 Footer.prototype = _.assignIn({}, Footer.prototype, { 7421 cacheSelectors: function() { 7422 this.cache = { 7423 $localeDisclosure: this.$container.find(selectors.disclosureLocale), 7424 $currencyDisclosure: this.$container.find(selectors.disclosureCurrency) 7425 }; 7426 }, 7427 7428 onUnload: function() { 7429 if (this.cache.$localeDisclosure.length) { 7430 this.localeDisclosure.unload(); 7431 } 7432 7433 if (this.cache.$currencyDisclosure.length) { 7434 this.currencyDisclosure.unload(); 7435 } 7436 } 7437 }); 7438 7439 return Footer; 7440})(); 7441 7442 7443$(document).ready(function() { 7444 var sections = new theme.Sections(); 7445 7446 sections.register('cart-template', theme.Cart); 7447 sections.register('product', theme.Product); 7448 sections.register('collection-template', theme.Filters); 7449 sections.register('product-template', theme.Product); 7450 sections.register('header-section', theme.HeaderSection); 7451 sections.register('map', theme.Maps); 7452 sections.register('slideshow-section', theme.SlideshowSection); 7453 sections.register('video-section', theme.VideoSection); 7454 sections.register('quotes', theme.Quotes); 7455 sections.register('hero-section', theme.HeroSection); 7456 sections.register('product-recommendations', theme.ProductRecommendations); 7457 sections.register('footer-section', theme.FooterSection); 7458}); 7459 7460theme.init = function() { 7461 theme.customerTemplates.init(); 7462 7463 // Theme-specific selectors to make tables scrollable 7464 var tableSelectors = '.rte table,' + '.custom__item-inner--html table'; 7465 7466 slate.rte.wrapTable({ 7467 $tables: $(tableSelectors), 7468 tableWrapperClass: 'scrollable-wrapper' 7469 }); 7470 7471 // Theme-specific selectors to make iframes responsive 7472 var iframeSelectors = 7473 '.rte iframe[src*="youtube.com/embed"],' + 7474 '.rte iframe[src*="player.vimeo"],' + 7475 '.custom__item-inner--html iframe[src*="youtube.com/embed"],' + 7476 '.custom__item-inner--html iframe[src*="player.vimeo"]'; 7477 7478 slate.rte.wrapIframe({ 7479 $iframes: $(iframeSelectors), 7480 iframeWrapperClass: 'video-wrapper' 7481 }); 7482 7483 // Common a11y fixes 7484 slate.a11y.pageLinkFocus($(window.location.hash)); 7485 7486 $('.in-page-link').on('click', function(evt) { 7487 slate.a11y.pageLinkFocus($(evt.currentTarget.hash)); 7488 }); 7489 7490 $('a[href="#"]').on('click', function(evt) { 7491 evt.preventDefault(); 7492 }); 7493 7494 slate.a11y.accessibleLinks({ 7495 messages: { 7496 newWindow: theme.strings.newWindow, 7497 external: theme.strings.external, 7498 newWindowExternal: theme.strings.newWindowExternal 7499 }, 7500 $links: $('a[href]:not([aria-describedby], .product-single__thumbnail)') 7501 }); 7502 7503 theme.FormStatus.init(); 7504 7505 var selectors = { 7506 image: '[data-image]', 7507 imagePlaceholder: '[data-image-placeholder]', 7508 imageWithPlaceholderWrapper: '[data-image-with-placeholder-wrapper]', 7509 lazyloaded: '.lazyloaded' 7510 }; 7511 7512 var classes = { 7513 hidden: 'hide' 7514 }; 7515 7516 $(document).on('lazyloaded', function(e) { 7517 var $target = $(e.target); 7518 7519 if ($target.data('bgset')) { 7520 var $image = $target.find(selectors.lazyloaded); 7521 if ($image.length) { 7522 var alt = $target.data('alt'); 7523 var src = $image.data('src') ? $image.data('src') : $target.data('bg'); 7524 7525 $image.attr('alt', alt ? alt : ''); 7526 $image.attr('src', src ? src : ''); 7527 } 7528 } 7529 7530 if (!$target.is(selectors.image)) { 7531 return; 7532 } 7533 7534 $target 7535 .closest(selectors.imageWithPlaceholderWrapper) 7536 .find(selectors.imagePlaceholder)
7537 .addClass(classes.hidden); 7538 }); 7539 7540 // When the theme loads, lazysizes might load images before the "lazyloaded" 7541 // event listener has been attached. When this happens, the following function 7542 // hides the loading placeholders. 7543 function onLoadHideLazysizesAnimation() { 7544 $(selectors.image + '.lazyloaded') 7545 .closest(selectors.imageWithPlaceholderWrapper) 7546 .find(selectors.imagePlaceholder) 7547 .addClass(classes.hidden); 7548 } 7549 7550 onLoadHideLazysizesAnimation(); 7551 $(document).one('touchstart', function() { 7552 theme.Helpers.setTouch(); 7553 }); 7554}; 7555 7556// Youtube API callback 7557// eslint-disable-next-line no-unused-vars 7558function onYouTubeIframeAPIReady() { 7559 theme.Video.loadVideos(); 7560 theme.ProductVideo.loadVideos(theme.ProductVideo.hosts.youtube); 7561} 7562 7563$(theme.init); 7564 7565// Info Tabs on Product page 7566 7567 $(document).ready(function() { 7568 $('ul.tabs').each(function(){ 7569 var active, content, links = $(this).find('a'); 7570 active = links.first().addClass('active'); 7571 content = $(active.attr('href')); 7572 links.not(':first').each(function () { 7573 $($(this).attr('href')).hide(); 7574 }); 7575 $(this).find('a').click(function(e){ 7576 active.removeClass('active'); 7577 content.hide(); 7578 active = $(this); 7579 content = $($(this).attr('href')); 7580 active.addClass('active'); 7581 content.show(); 7582 return false; 7583 }); 7584 }); 7585 }); 7586 7587// Custom size variants 7588// GR - 12 Jan'22 7589 7590$('.custom-size-button').on('click', function() { 7591 // add selected state 7592 if ( $(this).hasClass('selected') ) { 7593 $(this).removeClass('selected'); 7594 } else { 7595 $('.custom-size-button').removeClass('selected'); 7596 $(this).addClass('selected'); 7597 } 7598 // grab the value attribute 7599 let value = $(this).attr('data-value'); 7600 $('.custom-size-selector').val(value).trigger('change'); 7601})
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.