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 keyboardKeys: { 147 TAB: 9, 148 ENTER: 13, 149 ESCAPE: 27, 150 LEFTARROW: 37, 151 RIGHTARROW: 39 152 } 153}; 154 155window.slate = window.slate || {}; 156 157/** 158 * iFrames 159 * ----------------------------------------------------------------------------- 160 * Wrap videos in div to force responsive layout. 161 * 162 * @namespace iframes 163 */ 164 165slate.rte = { 166 /** 167 * Wrap tables in a container div to make them scrollable when needed 168 * 169 * @param {object} options - Options to be used 170 * @param {jquery} options.$tables - jquery object(s) of the table(s) to wrap 171 * @param {string} options.tableWrapperClass - table wrapper class name 172 */ 173 wrapTable: function(options) { 174 options.$tables.wrap( 175 '<div class="' + options.tableWrapperClass + '"></div>' 176 ); 177 }, 178 179 /** 180 * Wrap iframes in a container div to make them responsive 181 * 182 * @param {object} options - Options to be used 183 * @param {jquery} options.$iframes - jquery object(s) of the iframe(s) to wrap 184 * @param {string} options.iframeWrapperClass - class name used on the wrapping div 185 */ 186 wrapIframe: function(options) { 187 options.$iframes.each(function() { 188 // Add wrapper to make video responsive 189 $(this).wrap('<div class="' + options.iframeWrapperClass + '"></div>'); 190 191 // Re-set the src attribute on each iframe after page load 192 // for Chrome's "incorrect iFrame content on 'back'" bug. 193 // https://code.google.com/p/chromium/issues/detail?id=395791 194 // Need to specifically target video and admin bar 195 this.src = this.src; 196 }); 197 } 198}; 199 200window.slate = window.slate || {}; 201 202/** 203 * A11y Helpers 204 * ----------------------------------------------------------------------------- 205 * A collection of useful functions that help make your theme more accessible 206 * to users with visual impairments. 207 * 208 * 209 * @namespace a11y 210 */ 211 212slate.a11y = { 213 /** 214 * For use when focus shifts to a container rather than a link 215 * eg for In-page links, after scroll, focus shifts to content area so that 216 * next `tab` is where user expects if focusing a link, just $link.focus(); 217 * 218 * @param {JQuery} $element - The element to be acted upon 219 */ 220 pageLinkFocus: function($element) { 221 var focusClass = 'js-focus-hidden'; 222 223 $element 224 .first() 225 .attr('tabIndex', '-1') 226 .focus() 227 .addClass(focusClass) 228 .one('blur', callback); 229 230 function callback() { 231 $element 232 .first() 233 .removeClass(focusClass) 234 .removeAttr('tabindex'); 235 } 236 }, 237 238 /** 239 * If there's a hash in the url, focus the appropriate element 240 */ 241 focusHash: function() { 242 var hash = window.location.hash; 243 244 // is there a hash in the url? is it an element on the page? 245 if (hash && document.getElementById(hash.slice(1))) { 246 this.pageLinkFocus($(hash)); 247 } 248 }, 249 250 /** 251 * When an in-page (url w/hash) link is clicked, focus the appropriate element 252 */ 253 bindInPageLinks: function() { 254 $('a[href*=#]').on( 255 'click', 256 function(evt) { 257 this.pageLinkFocus($(evt.currentTarget.hash)); 258 }.bind(this) 259 ); 260 }, 261 262 /** 263 * Traps the focus in a particular container 264 * 265 * @param {object} options - Options to be used 266 * @param {jQuery} options.$container - Container to trap focus within 267 * @param {jQuery} options.$elementToFocus - Element to be focused when focus leaves container 268 * @param {string} options.namespace - Namespace used for new focus event handler 269 */ 270 trapFocus: function(options) { 271 var eventsName = { 272 focusin: options.namespace ? 'focusin.' + options.namespace : 'focusin', 273 focusout: options.namespace 274 ? 'focusout.' + options.namespace 275 : 'focusout', 276 keydown: options.namespace 277 ? 'keydown.' + options.namespace 278 : 'keydown.handleFocus' 279 }; 280 281 /** 282 * Get every possible visible focusable element 283 */
284 var $focusableElements = options.$container.find( 285 $( 286 'button, [href], input, select, textarea, [tabindex]:not([tabindex^="-"])' 287 ).filter(':visible') 288 ); 289 var firstFocusable = $focusableElements[0]; 290 var lastFocusable = $focusableElements[$focusableElements.length - 1]; 291 292 if (!options.$elementToFocus) { 293 options.$elementToFocus = options.$container; 294 } 295 296 function _manageFocus(evt) { 297 if (evt.keyCode !== slate.utils.keyboardKeys.TAB) return; 298 299 /** 300 * On the last focusable element and tab forward, 301 * focus the first element. 302 */ 303 if (evt.target === lastFocusable && !evt.shiftKey) { 304 evt.preventDefault(); 305 firstFocusable.focus(); 306 } 307 /** 308 * On the first focusable element and tab backward, 309 * focus the last element. 310 */ 311 if (evt.target === firstFocusable && evt.shiftKey) { 312 evt.preventDefault(); 313 lastFocusable.focus(); 314 } 315 } 316 317 options.$container.attr('tabindex', '-1'); 318 options.$elementToFocus.focus(); 319 320 $(document).off('focusin'); 321 322 $(document).on(eventsName.focusout, function() { 323 $(document).off(eventsName.keydown); 324 }); 325 326 $(document).on(eventsName.focusin, function(evt) { 327 if (evt.target !== lastFocusable && evt.target !== firstFocusable) return; 328 329 $(document).on(eventsName.keydown, function(evt) { 330 _manageFocus(evt); 331 }); 332 }); 333 }, 334 335 /** 336 * Removes the trap of focus in a particular container 337 * 338 * @param {object} options - Options to be used 339 * @param {jQuery} options.$container - Container to trap focus within 340 * @param {string} options.namespace - Namespace used for new focus event handler 341 */ 342 removeTrapFocus: function(options) { 343 var eventName = options.namespace 344 ? 'focusin.' + options.namespace 345 : 'focusin'; 346 347 if (options.$container && options.$container.length) { 348 options.$container.removeAttr('tabindex'); 349 } 350 351 $(document).off(eventName); 352 }, 353 354 /** 355 * Add aria-describedby attribute to external and new window links 356 * 357 * @param {object} options - Options to be used 358 * @param {object} options.messages - Custom messages to be used 359 * @param {jQuery} options.$links - Specific links to be targeted 360 */ 361 accessibleLinks: function(options) { 362 var body = document.querySelector('body'); 363 364 var idSelectors = { 365 newWindow: 'a11y-new-window-message', 366 external: 'a11y-external-message', 367 newWindowExternal: 'a11y-new-window-external-message' 368 }; 369 370 if (options.$links === undefined || !options.$links.jquery) { 371 options.$links = $('a[href]:not([aria-describedby])'); 372 } 373 374 function generateHTML(customMessages) { 375 if (typeof customMessages !== 'object') { 376 customMessages = {}; 377 } 378 379 var messages = $.extend( 380 { 381 newWindow: 'Opens in a new window.', 382 external: 'Opens external website.', 383 newWindowExternal: 'Opens external website in a new window.' 384 }, 385 customMessages 386 ); 387 388 var container = document.createElement('ul'); 389 var htmlMessages = ''; 390 391 for (var message in messages) { 392 htmlMessages += 393 '<li id=' + idSelectors[message] + '>' + messages[message] + '</li>'; 394 } 395 396 container.setAttribute('hidden', true); 397 container.innerHTML = htmlMessages; 398 399 body.appendChild(container); 400 } 401 402 function _externalSite($link) { 403 var hostname = window.location.hostname; 404 405 return $link[0].hostname !== hostname; 406 } 407 408 $.each(options.$links, function() { 409 var $link = $(this); 410 var target = $link.attr('target'); 411 var rel = $link.attr('rel'); 412 var isExternal = _externalSite($link); 413 var isTargetBlank = target === '_blank'; 414 415 if (isExternal) { 416 $link.attr('aria-describedby', idSelectors.external); 417 } 418 if (isTargetBlank) { 419 if (rel === undefined || rel.indexOf('noopener') === -1) { 420 $link.attr('rel', function(i, val) { 421 var relValue = val === undefined ? '' : val + ' '; 422 return relValue + 'noopener'; 423 }); 424 } 425 $link.attr('aria-describedby', idSelectors.newWindow); 426 } 427 if (isExternal && isTargetBlank) { 428 $link.attr('aria-describedby', idSelectors.newWindowExternal); 429 } 430 }); 431 432 generateHTML(options.messages); 433 } 434}; 435 436/** 437 * Image Helper Functions 438 * ----------------------------------------------------------------------------- 439 * A collection of functions that help with basic image operations. 440 * 441 */ 442 443theme.Images = (function() { 444 /** 445 * Preloads an image in memory and uses the browsers cache to store it until needed. 446 * 447 * @param {Array} images - A list of image urls 448 * @param {String} size - A shopify image size attribute 449 */ 450 451 function preload(images, size) { 452 if (typeof images === 'string') { 453 images = [images]; 454 } 455 456 for (var i = 0; i < images.length; i++) { 457 var image = images[i]; 458 this.loadImage(this.getSizedImageUrl(image, size)); 459 } 460 } 461 462 /** 463 * Loads and caches an image in the browsers cache. 464 * @param {string} path - An image url 465 */ 466 function loadImage(path) { 467 new Image().src = path; 468 } 469 470 /** 471 * Swaps the src of an image for another OR returns the imageURL to the callback function 472 * @param image 473 * @param element 474 * @param callback 475 */ 476 function switchImage(image, element, callback) { 477 var size = this.imageSize(element.src);
478 var imageUrl = this.getSizedImageUrl(image.src, size); 479 480 if (callback) { 481 callback(imageUrl, image, element); // eslint-disable-line callback-return 482 } else { 483 element.src = imageUrl; 484 } 485 } 486 487 /** 488 * +++ Useful 489 * Find the Shopify image attribute size 490 * 491 * @param {string} src 492 * @returns {null} 493 */ 494 function imageSize(src) { 495 var match = src.match( 496 /.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\\.@]/ 497 ); 498 499 if (match !== null) { 500 if (match[2] !== undefined) { 501 return match[1] + match[2]; 502 } else { 503 return match[1]; 504 } 505 } else { 506 return null; 507 } 508 } 509 510 /** 511 * +++ Useful 512 * Adds a Shopify size attribute to a URL 513 * 514 * @param src 515 * @param size 516 * @returns {*} 517 */ 518 function getSizedImageUrl(src, size) { 519 if (size === null) { 520 return src; 521 } 522 523 if (size === 'master') { 524 return this.removeProtocol(src); 525 } 526 527 var match = src.match( 528 /\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i 529 ); 530 531 if (match !== null) { 532 var prefix = src.split(match[0]); 533 var suffix = match[0]; 534 535 return this.removeProtocol(prefix[0] + '_' + size + suffix); 536 } 537 538 return null; 539 } 540 541 function removeProtocol(path) { 542 return path.replace(/http(s)?:/, ''); 543 } 544 545 return { 546 preload: preload, 547 loadImage: loadImage, 548 switchImage: switchImage, 549 imageSize: imageSize, 550 getSizedImageUrl: getSizedImageUrl, 551 removeProtocol: removeProtocol 552 }; 553})(); 554 555/** 556 * Currency Helpers 557 * ----------------------------------------------------------------------------- 558 * A collection of useful functions that help with currency formatting 559 * 560 * Current contents 561 * - formatMoney - Takes an amount in cents and returns it as a formatted dollar value. 562 * 563 * Alternatives 564 * - Accounting.js - http://openexchangerates.github.io/accounting.js/ 565 * 566 */ 567 568theme.Currency = (function() { 569 var moneyFormat = '${{amount}}'; // eslint-disable-line camelcase 570 571 function formatMoney(cents, format) { 572 if (typeof cents === 'string') { 573 cents = cents.replace('.', ''); 574 } 575 var value = ''; 576 var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/; 577 var formatString = format || moneyFormat; 578 579 function formatWithDelimiters(number, precision, thousands, decimal) { 580 thousands = thousands || ','; 581 decimal = decimal || '.'; 582 583 if (isNaN(number) || number === null) { 584 return 0; 585 } 586 587 number = (number / 100.0).toFixed(precision); 588 589 var parts = number.split('.'); 590 var dollarsAmount = parts[0].replace( 591 /(\d)(?=(\d\d\d)+(?!\d))/g, 592 '$1' + thousands 593 ); 594 var centsAmount = parts[1] ? decimal + parts[1] : ''; 595 596 return dollarsAmount + centsAmount; 597 } 598 599 switch (formatString.match(placeholderRegex)[1]) { 600 case 'amount': 601 value = formatWithDelimiters(cents, 2); 602 break; 603 case 'amount_no_decimals': 604 value = formatWithDelimiters(cents, 0); 605 break; 606 case 'amount_with_comma_separator': 607 value = formatWithDelimiters(cents, 2, '.', ','); 608 break; 609 case 'amount_no_decimals_with_comma_separator': 610 value = formatWithDelimiters(cents, 0, '.', ','); 611 break; 612 case 'amount_no_decimals_with_space_separator': 613 value = formatWithDelimiters(cents, 0, ' '); 614 break; 615 case 'amount_with_apostrophe_separator': 616 value = formatWithDelimiters(cents, 2, "'"); 617 break; 618 } 619 620 return formatString.replace(placeholderRegex, value); 621 } 622 623 return { 624 formatMoney: formatMoney 625 }; 626})(); 627 628/** 629 * Variant Selection scripts 630 * ------------------------------------------------------------------------------ 631 * 632 * Handles change events from the variant inputs in any `cart/add` forms that may 633 * exist. Also updates the master select and triggers updates when the variants 634 * price or image changes. 635 * 636 * @namespace variants 637 */ 638 639slate.Variants = (function() { 640 /** 641 * Variant constructor 642 * 643 * @param {object} options - Settings from `product.js` 644 */ 645 function Variants(options) { 646 this.$container = options.$container; 647 this.product = options.product; 648 this.singleOptionSelector = options.singleOptionSelector; 649 this.originalSelectorId = options.originalSelectorId; 650 this.enableHistoryState = options.enableHistoryState; 651 this.currentVariant = this._getVariantFromOptions(); 652 653 $(this.singleOptionSelector, this.$container).on( 654 'change', 655 this._onSelectChange.bind(this) 656 ); 657 } 658 659 Variants.prototype = _.assignIn({}, Variants.prototype, { 660 /** 661 * Get the currently selected options from add-to-cart form. Works with all 662 * form input elements. 663 * 664 * @return {array} options - Values of currently selected variants 665 */ 666 _getCurrentOptions: function() { 667 var currentOptions = _.map( 668 $(this.singleOptionSelector, this.$container), 669 function(element) { 670 var $element = $(element); 671 var type = $element.attr('type'); 672 var currentOption = {}; 673 674 if (type === 'radio' || type === 'checkbox') { 675 if ($element[0].checked) { 676 currentOption.value = $element.val(); 677 currentOption.index = $element.data('index'); 678 679 return currentOption; 680 } else { 681 return false;
682 } 683 } else { 684 currentOption.value = $element.val(); 685 currentOption.index = $element.data('index'); 686 687 return currentOption; 688 } 689 } 690 ); 691 692 // remove any unchecked input values if using radio buttons or checkboxes 693 currentOptions = _.compact(currentOptions); 694 695 return currentOptions; 696 }, 697 698 /** 699 * Find variant based on selected values. 700 * 701 * @param {array} selectedValues - Values of variant inputs 702 * @return {object || undefined} found - Variant object from product.variants 703 */ 704 _getVariantFromOptions: function() { 705 var selectedValues = this._getCurrentOptions(); 706 var variants = this.product.variants; 707 708 var found = _.find(variants, function(variant) { 709 return selectedValues.every(function(values) { 710 return _.isEqual(variant[values.index], values.value); 711 }); 712 }); 713 714 return found; 715 }, 716 717 /** 718 * Event handler for when a variant input changes. 719 */ 720 _onSelectChange: function() { 721 var variant = this._getVariantFromOptions(); 722 723 this.$container.trigger({ 724 type: 'variantChange', 725 variant: variant 726 }); 727 728 if (!variant) { 729 return; 730 } 731 732 this._updateMasterSelect(variant); 733 this._updateImages(variant); 734 this._updatePrice(variant); 735 this._updateSKU(variant); 736 this.currentVariant = variant; 737 738 if (this.enableHistoryState) { 739 this._updateHistoryState(variant); 740 } 741 }, 742 743 /** 744 * Trigger event when variant image changes 745 * 746 * @param {object} variant - Currently selected variant 747 * @return {event} variantImageChange 748 */ 749 _updateImages: function(variant) { 750 var variantImage = variant.featured_image || {}; 751 var currentVariantImage = this.currentVariant.featured_image || {}; 752 753 if ( 754 !variant.featured_image || 755 variantImage.src === currentVariantImage.src 756 ) { 757 return; 758 } 759 760 this.$container.trigger({ 761 type: 'variantImageChange', 762 variant: variant 763 }); 764 }, 765 766 /** 767 * Trigger event when variant price changes. 768 * 769 * @param {object} variant - Currently selected variant 770 * @return {event} variantPriceChange 771 */ 772 _updatePrice: function(variant) { 773 if ( 774 variant.price === this.currentVariant.price && 775 variant.compare_at_price === this.currentVariant.compare_at_price 776 ) { 777 return; 778 } 779 780 this.$container.trigger({ 781 type: 'variantPriceChange', 782 variant: variant 783 }); 784 }, 785 786 /** 787 * Trigger event when variant sku changes. 788 * 789 * @param {object} variant - Currently selected variant 790 * @return {event} variantSKUChange 791 */ 792 _updateSKU: function(variant) { 793 if (variant.sku === this.currentVariant.sku) { 794 return; 795 } 796 797 this.$container.trigger({ 798 type: 'variantSKUChange', 799 variant: variant 800 }); 801 }, 802 803 /** 804 * Update history state for product deeplinking 805 * 806 * @param {variant} variant - Currently selected variant 807 * @return {k} [description] 808 */ 809 _updateHistoryState: function(variant) { 810 if (!history.replaceState || !variant) { 811 return; 812 } 813 814 var newurl = 815 window.location.protocol + 816 '//' + 817 window.location.host + 818 window.location.pathname + 819 '?variant=' + 820 variant.id; 821 window.history.propertyIsEnumerable({ path: newurl }, '', newurl); 822 }, 823 824 /** 825 * Update hidden master select of variant change 826 * 827 * @param {variant} variant - Currently selected variant 828 */ 829 _updateMasterSelect: function(variant) { 830 $(this.originalSelectorId, this.$container).val(variant.id); 831 } 832 }); 833 834 return Variants; 835})(); 836 837 838/* ================ GLOBAL ================ */ 839/*============================================================================ 840 Drawer modules 841==============================================================================*/ 842theme.Drawers = (function() { 843 function Drawer(id, position, options) { 844 var defaults = { 845 close: '.js-drawer-close', 846 open: '.js-drawer-open-' + position, 847 openClass: 'js-drawer-open', 848 dirOpenClass: 'js-drawer-open-' + position 849 }; 850 851 this.nodes = { 852 $parent: $('html').add('body'), 853 $page: $('#PageContainer') 854 }; 855 856 this.config = $.extend(defaults, options); 857 this.position = position; 858 859 this.$drawer = $('#' + id); 860 861 if (!this.$drawer.length) { 862 return false;
863 } 864 865 this.drawerIsOpen = false; 866 this.init(); 867 } 868 869 Drawer.prototype.init = function() { 870 $(this.config.open).on('click', $.proxy(this.open, this)); 871 this.$drawer.on('click', this.config.close, $.proxy(this.close, this)); 872 }; 873 874 Drawer.prototype.open = function(evt) { 875 // Keep track if drawer was opened from a click, or called by another function 876 var externalCall = false; 877 878 // Prevent following href if link is clicked 879 if (evt) { 880 evt.preventDefault(); 881 } else { 882 externalCall = true; 883 } 884 885 // Without this, the drawer opens, the click event bubbles up to nodes.$page 886 // which closes the drawer. 887 if (evt && evt.stopPropagation) { 888 evt.stopPropagation(); 889 // save the source of the click, we'll focus to this on close 890 this.$activeSource = $(evt.currentTarget); 891 } 892 893 if (this.drawerIsOpen && !externalCall) { 894 return this.close(); 895 } 896 897 // Add is-transitioning class to moved elements on open so drawer can have 898 // transition for close animation 899 this.$drawer.prepareTransition(); 900 901 this.nodes.$parent.addClass( 902 this.config.openClass + ' ' + this.config.dirOpenClass 903 ); 904 this.drawerIsOpen = true; 905 906 // Set focus on drawer 907 slate.a11y.trapFocus({ 908 $container: this.$drawer, 909 namespace: 'drawer_focus' 910 }); 911 912 // Run function when draw opens if set 913 if ( 914 this.config.onDrawerOpen && 915 typeof this.config.onDrawerOpen === 'function' 916 ) { 917 if (!externalCall) { 918 this.config.onDrawerOpen(); 919 } 920 } 921 922 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { 923 this.$activeSource.attr('aria-expanded', 'true'); 924 } 925 926 this.bindEvents(); 927 928 return this; 929 }; 930 931 Drawer.prototype.close = function() { 932 if (!this.drawerIsOpen) { 933 // don't close a closed drawer 934 return; 935 } 936 937 // deselect any focused form elements 938 $(document.activeElement).trigger('blur'); 939 940 // Ensure closing transition is applied to moved elements, like the nav 941 this.$drawer.prepareTransition(); 942 943 this.nodes.$parent.removeClass( 944 this.config.dirOpenClass + ' ' + this.config.openClass 945 ); 946 947 if (this.$activeSource && this.$activeSource.attr('aria-expanded')) { 948 this.$activeSource.attr('aria-expanded', 'false'); 949 } 950 951 this.drawerIsOpen = false; 952 953 // Remove focus on drawer 954 slate.a11y.removeTrapFocus({ 955 $container: this.$drawer, 956 namespace: 'drawer_focus' 957 }); 958 959 this.unbindEvents(); 960 961 // Run function when draw closes if set 962 if ( 963 this.config.onDrawerClose && 964 typeof this.config.onDrawerClose === 'function' 965 ) { 966 this.config.onDrawerClose(); 967 } 968 }; 969 970 Drawer.prototype.bindEvents = function() { 971 this.nodes.$parent.on( 972 'keyup.drawer', 973 $.proxy(function(evt) { 974 // close on 'esc' keypress 975 if (evt.keyCode === 27) { 976 this.close(); 977 return false; 978 } else { 979 return true; 980 } 981 }, this) 982 ); 983 984 // Lock scrolling on mobile 985 this.nodes.$page.on('touchmove.drawer', function() { 986 return false; 987 }); 988 989 this.nodes.$page.on( 990 'click.drawer', 991 $.proxy(function() { 992 this.close(); 993 return false; 994 }, this) 995 ); 996 }; 997 998 Drawer.prototype.unbindEvents = function() { 999 this.nodes.$page.off('.drawer'); 1000 this.nodes.$parent.off('.drawer'); 1001 }; 1002 1003 return Drawer; 1004})(); 1005 1006 1007/* ================ MODULES ================ */ 1008window.theme = window.theme || {}; 1009 1010theme.Header = (function() { 1011 var selectors = { 1012 body: 'body', 1013 multicurrencySelector: '[data-currency-selector]', 1014 navigation: '#AccessibleNav', 1015 siteNavHasDropdown: '[data-has-dropdowns]', 1016 siteNavChildLinks: '.site-nav__child-link', 1017 siteNavActiveDropdown: '.site-nav--active-dropdown', 1018 siteNavHasCenteredDropdown: '.site-nav--has-centered-dropdown', 1019 siteNavCenteredDropdown: '.site-nav__dropdown--centered', 1020 siteNavLinkMain: '.site-nav__link--main', 1021 siteNavChildLink: '.site-nav__link--last', 1022 siteNavDropdown: '.site-nav__dropdown', 1023 siteHeader: '.site-header' 1024 }; 1025 1026 var config = { 1027 activeClass: 'site-nav--active-dropdown', 1028 childLinkClass: 'site-nav__child-link', 1029 rightDropdownClass: 'site-nav__dropdown--right', 1030 leftDropdownClass: 'site-nav__dropdown--left' 1031 }; 1032 1033 var cache = {}; 1034
1035 function init() { 1036 cacheSelectors(); 1037 styleDropdowns($(selectors.siteNavHasDropdown)); 1038 positionFullWidthDropdowns(); 1039 1040 cache.$parents.on('click.siteNav', function() { 1041 var $el = $(this); 1042 $el.hasClass(config.activeClass) ? hideDropdown($el) : showDropdown($el); 1043 }); 1044 1045 // check when we're leaving a dropdown and close the active dropdown 1046 $(selectors.siteNavChildLink).on('focusout.siteNav', function() { 1047 setTimeout(function() { 1048 if ( 1049 $(document.activeElement).hasClass(config.childLinkClass) || 1050 !cache.$activeDropdown.length 1051 ) { 1052 return; 1053 } 1054 1055 hideDropdown(cache.$activeDropdown); 1056 }); 1057 }); 1058 1059 // close dropdowns when on top level nav 1060 cache.$topLevel.on('focus.siteNav', function() { 1061 if (cache.$activeDropdown.length) { 1062 hideDropdown(cache.$activeDropdown); 1063 } 1064 }); 1065 1066 cache.$subMenuLinks.on('click.siteNav', function(evt) { 1067 // Prevent click on body from firing instead of link 1068 evt.stopImmediatePropagation(); 1069 }); 1070 1071 $(selectors.multicurrencySelector).on('change', function() { 1072 $(this) 1073 .parents('form') 1074 .submit(); 1075 }); 1076 1077 $(window).resize( 1078 $.debounce(50, function() { 1079 styleDropdowns($(selectors.siteNavHasDropdown)); 1080 positionFullWidthDropdowns(); 1081 }) 1082 ); 1083 } 1084 1085 function cacheSelectors() { 1086 cache = { 1087 $nav: $(selectors.navigation), 1088 $topLevel: $(selectors.siteNavLinkMain), 1089 $parents: $(selectors.navigation).find(selectors.siteNavHasDropdown), 1090 $subMenuLinks: $(selectors.siteNavChildLinks), 1091 $activeDropdown: $(selectors.siteNavActiveDropdown), 1092 $siteHeader: $(selectors.siteHeader) 1093 }; 1094 } 1095 1096 function showDropdown($el) { 1097 $el.addClass(config.activeClass); 1098 1099 // close open dropdowns 1100 if (cache.$activeDropdown.length) { 1101 hideDropdown(cache.$activeDropdown); 1102 } 1103 1104 cache.$activeDropdown = $el; 1105 1106 // set expanded on open dropdown 1107 $el.find(selectors.siteNavLinkMain).attr('aria-expanded', 'true'); 1108 1109 setTimeout(function() { 1110 $(window).on('keyup.siteNav', function(evt) { 1111 if (evt.keyCode === 27) { 1112 hideDropdown($el); 1113 } 1114 }); 1115 1116 $(selectors.body).on('click.siteNav', function() { 1117 hideDropdown($el); 1118 }); 1119 }, 250); 1120 } 1121 1122 function hideDropdown($el) { 1123 // remove aria on open dropdown 1124 $el.find(selectors.siteNavLinkMain).attr('aria-expanded', 'false'); 1125 $el.removeClass(config.activeClass); 1126 1127 // reset active dropdown 1128 cache.$activeDropdown = $(selectors.siteNavActiveDropdown); 1129 1130 $(selectors.body).off('click.siteNav'); 1131 $(window).off('keyup.siteNav'); 1132 } 1133 1134 function styleDropdowns($dropdownListItems) { 1135 $dropdownListItems.each(function() { 1136 var $dropdownLi = $(this).find(selectors.siteNavDropdown); 1137 if (!$dropdownLi.length) { 1138 return; 1139 } 1140 var isRightOfLogo = 1141 Math.ceil($(this).offset().left) > 1142 Math.floor(cache.$siteHeader.outerWidth()) / 2 1143 ? true 1144 : false; 1145 if (isRightOfLogo) { 1146 $dropdownLi 1147 .removeClass(config.leftDropdownClass) 1148 .addClass(config.rightDropdownClass); 1149 } else { 1150 $dropdownLi 1151 .removeClass(config.rightDropdownClass) 1152 .addClass(config.leftDropdownClass); 1153 } 1154 }); 1155 } 1156 1157 function positionFullWidthDropdowns() { 1158 var $listWithCenteredDropdown = $(selectors.siteNavHasCenteredDropdown); 1159 1160 $listWithCenteredDropdown.each(function() { 1161 var $hasCenteredDropdown = $(this); 1162 var $fullWidthDropdown = $hasCenteredDropdown.find( 1163 selectors.siteNavCenteredDropdown 1164 ); 1165 1166 var fullWidthDropdownOffset = $hasCenteredDropdown.position().top + 41; 1167 $fullWidthDropdown.css('top', fullWidthDropdownOffset); 1168 }); 1169 } 1170 1171 function unload() { 1172 $(window).off('.siteNav'); 1173 cache.$parents.off('.siteNav'); 1174 cache.$subMenuLinks.off('.siteNav'); 1175 cache.$topLevel.off('.siteNav'); 1176 $(selectors.siteNavChildLink).off('.siteNav'); 1177 $(selectors.body).off('.siteNav'); 1178 } 1179 1180 return { 1181 init: init, 1182 unload: unload 1183 }; 1184})(); 1185 1186window.theme = window.theme || {}; 1187 1188theme.MobileNav = (function() { 1189 var classes = { 1190 mobileNavOpenIcon: 'mobile-nav--open', 1191 mobileNavCloseIcon: 'mobile-nav--close', 1192 navLinkWrapper: 'mobile-nav__item', 1193 navLink: 'mobile-nav__link', 1194 subNavLink: 'mobile-nav__sublist-link', 1195 return: 'mobile-nav__return-btn', 1196 subNavActive: 'is-active', 1197 subNavClosing: 'is-closing', 1198 navOpen: 'js-menu--is-open', 1199 subNavShowing: 'sub-nav--is-open', 1200 thirdNavShowing: 'third-nav--is-open', 1201 subNavToggleBtn: 'js-toggle-submenu' 1202 }; 1203 var cache = {}; 1204 var isTransitioning; 1205 var $activeSubNav; 1206 var $activeTrigger; 1207 var menuLevel = 1;
1208 // Breakpoints from src/stylesheets/global/variables.scss.liquid 1209 var mediaQuerySmall = 'screen and (max-width: 749px)'; 1210 1211 function init() { 1212 cacheSelectors(); 1213 1214 cache.$mobileNavToggle.on('click', toggleMobileNav); 1215 cache.$subNavToggleBtn.on('click.subNav', toggleSubNav); 1216 1217 // Close mobile nav when unmatching mobile breakpoint 1218 enquire.register(mediaQuerySmall, { 1219 unmatch: function() { 1220 if (cache.$mobileNavContainer.hasClass(classes.navOpen)) { 1221 closeMobileNav(); 1222 } 1223 } 1224 }); 1225 } 1226 1227 function toggleMobileNav() { 1228 if (cache.$mobileNavToggle.hasClass(classes.mobileNavCloseIcon)) { 1229 closeMobileNav(); 1230 } else { 1231 openMobileNav(); 1232 } 1233 } 1234 1235 function cacheSelectors() { 1236 cache = { 1237 $pageContainer: $('#PageContainer'), 1238 $siteHeader: $('.site-header'), 1239 $mobileNavToggle: $('.js-mobile-nav-toggle'), 1240 $mobileNavContainer: $('.mobile-nav-wrapper'), 1241 $mobileNav: $('#MobileNav'), 1242 $sectionHeader: $('#shopify-section-header'), 1243 $subNavToggleBtn: $('.' + classes.subNavToggleBtn) 1244 }; 1245 } 1246 1247 function openMobileNav() { 1248 var translateHeaderHeight = cache.$siteHeader.outerHeight(); 1249 1250 cache.$mobileNavContainer.prepareTransition().addClass(classe
1250s.navOpen); 1251 1252 cache.$mobileNavContainer.css({ 1253 transform: 'translateY(' + translateHeaderHeight + 'px)' 1254 }); 1255 1256 cache.$pageContainer.css({ 1257 transform: 1258 'translate3d(0, ' + cache.$mobileNavContainer[0].scrollHeight + 'px, 0)' 1259 }); 1260 1261 slate.a11y.trapFocus({ 1262 $container: cache.$sectionHeader, 1263 $elementToFocus: cache.$mobileNavToggle, 1264 namespace: 'navFocus' 1265 }); 1266 1267 cache.$mobileNavToggle 1268 .addClass(classes.mobileNavCloseIcon) 1269 .removeClass(classes.mobileNavOpenIcon) 1270 .attr('aria-expanded', true); 1271 1272 // close on escape 1273 $(window).on('keyup.mobileNav', function(evt) { 1274 if (evt.which === 27) { 1275 closeMobileNav(); 1276 } 1277 }); 1278 } 1279 1280 function closeMobileNav() { 1281 cache.$mobileNavContainer.prepareTransition().removeClass(classes.navOpen); 1282 1283 cache.$mobileNavContainer.css({ 1284 transform: 'translateY(-100%)' 1285 }); 1286 1287 cache.$pageContainer.removeAttr('style'); 1288 1289 slate.a11y.trapFocus({ 1290 $container: $('html'), 1291 $elementToFocus: $('body') 1292 }); 1293 1294 cache.$mobileNavContainer.one( 1295 'TransitionEnd.navToggle webkitTransitionEnd.navToggle transitionend.navToggle oTransitionEnd.navToggle', 1296 function() { 1297 slate.a11y.removeTrapFocus({ 1298 $container: cache.$mobileNav, 1299 namespace: 'navFocus' 1300 }); 1301 } 1302 ); 1303 1304 cache.$mobileNavToggle
1305 .addClass(classes.mobileNavOpenIcon) 1306 .removeClass(classes.mobileNavCloseIcon) 1307 .attr('aria-expanded', false) 1308 .focus(); 1309 1310 $(window).off('keyup.mobileNav'); 1311 1312 scrollTo(0, 0); 1313 } 1314 1315 function toggleSubNav(evt) { 1316 if (isTransitioning) { 1317 return; 1318 } 1319 1320 var $toggleBtn = $(evt.currentTarget); 1321 var isReturn = $toggleBtn.hasClass(classes.return); 1322 isTransitioning = true; 1323 1324 if (isReturn) { 1325 // Close all subnavs by removing active class on buttons 1326 $( 1327 '.' + classes.subNavToggleBtn + '[data-level="' + (menuLevel - 1) + '"]' 1328 ).removeClass(classes.subNavActive); 1329 1330 if ($activeTrigger && $activeTrigger.length) { 1331 $activeTrigger.removeClass(classes.subNavActive); 1332 } 1333 } else { 1334 $toggleBtn.addClass(classes.subNavActive); 1335 } 1336 1337 $activeTrigger = $toggleBtn; 1338 1339 goToSubnav($toggleBtn.data('target')); 1340 } 1341 1342 function goToSubnav(target) { 1343 /*eslint-disable shopify/jquery-dollar-sign-reference */ 1344 1345 var $targetMenu = target 1346 ? $('.mobile-nav__dropdown[data-parent="' + target + '"]') 1347 : cache.$mobileNav; 1348 1349 menuLevel = $targetMenu.data('level') ? $targetMenu.data('level') : 1; 1350 1351 if ($activeSubNav && $activeSubNav.length) {
1352 $activeSubNav.prepareTransition().addClass(classes.subNavClosing); 1353 } 1354 1355 $activeSubNav = $targetMenu; 1356 1357 /*eslint-enable shopify/jquery-dollar-sign-reference */ 1358 1359 var translateMenuHeight = $targetMenu.outerHeight(); 1360 1361 var openNavClass = 1362 menuLevel > 2 ? classes.thirdNavShowing : classes.subNavShowing; 1363 1364 cache.$mobileNavContainer 1365 .css('height', translateMenuHeight) 1366 .removeClass(classes.thirdNavShowing) 1367 .addClass(openNavClass); 1368 1369 if (!target) { 1370 // Show top level nav 1371 cache.$mobileNavContainer 1372 .removeClass(classes.thirdNavShowing) 1373 .removeClass(classes.subNavShowing); 1374 } 1375 1376 /* if going back to first subnav, focus is on whole header */ 1377 var $container = menuLevel === 1 ? cache.$sectionHeader : $targetMenu; 1378 1379 var $menuTitle = $targetMenu.find('[data-menu-title=' + menuLevel + ']'); 1380 var $elementToFocus = $menuTitle ? $menuTitle : $targetMenu; 1381 1382 // Focusing an item in the subnav early forces element into view and breaks the animation. 1383 cache.$mobileNavContainer.one( 1384 'TransitionEnd.subnavToggle webkitTransitionEnd.subnavToggle transitionend.subnavToggle oTransitionEnd.subnavToggle', 1385 function() { 1386 slate.a11y.trapFocus({ 1387 $container: $container, 1388 $elementToFocus: $elementToFocus, 1389 namespace: 'subNavFocus' 1390 }); 1391 1392 cache.$mobileNavContainer.off('.subnavToggle'); 1393 isTransitioning = false; 1394 } 1395 ); 1396 1397 // Match height of subnav 1398 cache.$pageContainer.css({ 1399 transform: 'translateY(' + translateMenuHeight + 'px)' 1400 }); 1401 1402 $activeSubNav.removeClass(classes.subNavClosing); 1403 } 1404 1405 return { 1406 init: init, 1407 closeMobileNav: closeMobileNav 1408 }; 1409})(jQuery); 1410 1411window.theme = window.theme || {}; 1412 1413theme.Search = (function() { 1414 var selectors = { 1415 search: '.search', 1416 searchSubmit: '.search__submit', 1417 searchInput: '.search__input', 1418 1419 siteHeader: '.site-header', 1420 siteHeaderSearchToggle: '.site-header__search-toggle', 1421 siteHeaderSearch: '.site-header__search', 1422 1423 searchDrawer: '.search-bar', 1424 searchDrawerInput: '.search-bar__input', 1425 1426 searchHeader: '.search-header', 1427 searchHeaderInput: '.search-header__input', 1428 searchHeaderSubmit: '.search-header__submit', 1429 1430 searchResultSubmit: '#SearchResultSubmit', 1431 searchResultInput: '#SearchInput', 1432 searchResultMessage: '[data-search-error-message]', 1433 1434 mobileNavWrapper: '.mobile-nav-wrapper' 1435 }; 1436 1437 var classes = { 1438 focus: 'search--focus', 1439 hidden: 'hide', 1440 mobileNavIsOpen: 'js-menu--is-open', 1441 searchTemplate: 'template-search' 1442 }; 1443 1444 function init() { 1445 if (!$(selectors.siteHeader).length) { 1446 return; 1447 } 1448 1449 this.$searchResultInput = $(selectors.searchResultInput); 1450 this.$searchErrorMessage = $(selectors.searchResultMessage); 1451 1452 initDrawer(); 1453 1454 var isSearchPage = 1455 slate.utils.getParameterByName('q') !== null &&
1456 $('body').hasClass(classes.searchTemplate); 1457 1458 if (isSearchPage) { 1459 validateSearchResultForm.call(this); 1460 } 1461 1462 $(selectors.searchResultSubmit).on( 1463 'click', 1464 validateSearchResultForm.bind(this) 1465 ); 1466 1467 $(selectors.searchHeaderInput) 1468 .add(selectors.searchHeaderSubmit) 1469 .on('focus blur', function() { 1470 $(selectors.searchHeader).toggleClass(classes.focus); 1471 }); 1472 1473 $(selectors.siteHeaderSearchToggle).on('click', function() { 1474 var searchHeight = $(selectors.siteHeader).outerHeight(); 1475 var searchOffset = $(selectors.siteHeader).offset().top - searchHeight; 1476 1477 $(selectors.searchDrawer).css({ 1478 height: searchHeight + 'px', 1479 top: searchOffset + 'px' 1480 }); 1481 }); 1482 } 1483 1484 function initDrawer() { 1485 // Add required classes to HTML 1486 $('#PageContainer').addClass('drawer-page-content'); 1487 $('.js-drawer-open-top') 1488 .attr('aria-controls', 'SearchDrawer') 1489 .attr('aria-expanded', 'false') 1490 .attr('aria-haspopup', 'dialog'); 1491 1492 theme.SearchDrawer = new theme.Drawers('SearchDrawer', 'top', { 1493 onDrawerOpen: searchDrawerFocus, 1494 onDrawerClose: searchDrawerFocusClose 1495 }); 1496 } 1497 1498 function searchDrawerFocus() { 1499 searchFocus($(selectors.searchDrawerInput)); 1500
1501 if ($(selectors.mobileNavWrapper).hasClass(classes.mobileNavIsOpen)) { 1502 theme.MobileNav.closeMobileNav(); 1503 } 1504 } 1505 1506 function searchFocus($el) { 1507 $el.focus(); 1508 // set selection range hack for iOS 1509 $el[0].setSelectionRange(0, $el[0].value.length); 1510 } 1511 1512 function searchDrawerFocusClose() { 1513 $(selectors.siteHeaderSearchToggle).focus(); 1514 } 1515 1516 /** 1517 * Remove the aria-attributes and hide the error messages 1518 */ 1519 function hideErrorMessage() { 1520 this.$searchErrorMessage.addClass(classes.hidden); 1521 this.$searchResultInput 1522 .removeAttr('aria-describedby') 1523 .removeAttr('aria-invalid'); 1524 } 1525 1526 /** 1527 * Add the aria-attributes and show the error messages 1528 */ 1529 function showErrorMessage() { 1530 this.$searchErrorMessage.removeClass(classes.hidden); 1531 this.$searchResultInput 1532 .attr('aria-describedby', 'error-search-form') 1533 .attr('aria-invalid', true); 1534 } 1535 1536 function validateSearchResultForm(evt) { 1537 var isInputValueEmpty = this.$searchResultInput.val().trim().length === 0; 1538 1539 if (!isInputValueEmpty) { 1540 hideErrorMessage.call(this); 1541 return; 1542 } 1543 1544 if (typeof evt !== 'undefined') { 1545 evt.preventDefault(); 1546 } 1547 1548 searchFocus(this.$searchResultInput); 1549 showErrorMessage.call(this); 1550 } 1551 1552 return { 1553 init: init 1554 }; 1555})(); 1556 1557(function() { 1558 var selectors = { 1559 backButton: '.return-link' 1560 }; 1561 1562 var $backButton = $(selectors.backButton); 1563 1564 if (!document.referrer || !$backButton.length || !window.history.length) { 1565 return; 1566 } 1567 1568 $backButton.one('click', function(evt) { 1569 evt.preventDefault(); 1570 1571 var referrerDomain = urlDomain(document.referrer); 1572 var shopDomain = urlDomain(window.location.href); 1573 1574 if (shopDomain === referrerDomain) { 1575 history.back(); 1576 } 1577 1578 return false; 1579 }); 1580 1581 function urlDomain(url) { 1582 var anchor = document.createElement('a'); 1583 anchor.ref = url; 1584 1585 return anchor.hostname; 1586 } 1587})(); 1588 1589theme.Slideshow = (function() { 1590 this.$slideshow = null; 1591 var classes = { 1592 slideshow: 'slideshow', 1593 slickActiveMobile: 'slick-active-mobile', 1594 controlsHover: 'slideshow__controls--hover', 1595 isPaused: 'is-paused' 1596 }; 1597 1598 var selectors = { 1599 section: '.shopify-section', 1600 wrapper: '#SlideshowWrapper-', 1601 slides: '.slideshow__slide', 1602 textWrapperMobile: '.slideshow__text-wrap--mobile', 1603 textContentMobile: '.slideshow__text-content--mobile', 1604 controls: '.slideshow__controls', 1605 pauseButton: '.slideshow__pause', 1606 dots: '.slick-dots', 1607 arrows: '.slideshow__arrows', 1608 arrowsMobile: '.slideshow__arrows--mobile', 1609 arrowLeft: '.slideshow__arrow-left', 1610 arrowRight: '.slideshow__arrow-right' 1611 }; 1612 1613 function slideshow(el, sectionId) { 1614 var $slideshow = (this.$slideshow = $(el)); 1615 this.adaptHeight = this.$slideshow.data('adapt-height'); 1616 this.$wrapper = this.$slideshow.closest(selectors.wrapper + sectionId); 1617 this.$section = this.$wrapper.closest(selectors.section); 1618 this.$controls = this.$wrapper.find(selectors.controls); 1619 this.$arrows = this.$section.find(selectors.arrows); 1620 this.$arrowsMobile = this.$section.find(selectors.arrowsMobile); 1621 this.$pause = this.$controls.find(selectors.pauseButton); 1622 this.$textWrapperMobile = this.$section.find(selectors.textWrapperMobile); 1623 this.autorotate = this.$slideshow.data('autorotate'); 1624 var autoplaySpeed = this.$slideshow.data('speed'); 1625 var loadSlideA11yString = this.$slideshow.data('slide-nav-a11y'); 1626 1627 this.settings = { 1628 accessibility: true, 1629 arrows: false, 1630 dots: true, 1631 fade: true, 1632 draggable: true, 1633 touchThreshold: 20, 1634 autoplay: this.autorotate, 1635 autoplaySpeed: autoplaySpeed, 1636 // eslint-disable-next-line shopify/jquery-dollar-sign-reference 1637 appendDots: this.$arrows, 1638 customPaging: function(slick, index) { 1639 return ( 1640 '<a href="' + 1641 selectors.wrapper + 1642 sectionId + 1643 '" aria-label="' + 1644 loadSlideA11yString.replace('[slide_number]', index + 1) + 1645 '" data-slide-number="' + 1646 index + 1647 '"></a>' 1648 ); 1649 } 1650 }; 1651 1652 this.$slideshow.on('beforeChange', beforeChange.bind(this)); 1653 this.$slideshow.on('init', slideshowA11ySetup.bind(this)); 1654 1655 // Add class to style mobile dots & show the correct text content for the 1656 // first slide on mobile when the slideshow initialises 1657 this.$slideshow.on( 1658 'init', 1659 function() { 1660 this.$mobileDots 1661 .find('li:first-of-type')
1662 .addClass(classes.slickActiveMobile); 1663 this.showMobileText(0); 1664 }.bind(this) 1665 ); 1666 1667 // Stop the autorotate when you scroll past the mobile controls, resume when 1668 // they are scrolled back into view 1669 if (this.autorotate) { 1670 $(document).scroll( 1671 $.debounce( 1672 250, 1673 function() { 1674 if ( 1675 this.$arrowsMobile.offset().top + 1676 this.$arrowsMobile.outerHeight() < 1677 window.pageYOffset 1678 ) { 1679 $slideshow.slick('slickPause'); 1680 } else if (!this.$pause.hasClass(classes.isPaused)) { 1681 $slideshow.slick('slickPlay'); 1682 } 1683 }.bind(this) 1684 ) 1685 ); 1686 } 1687 1688 if (this.adaptHeight) { 1689 this.setSlideshowHeight(); 1690 $(window).resize($.debounce(50, this.setSlideshowHeight.bind(this))); 1691 } 1692 1693 this.$slideshow.slick(this.settings); 1694 1695 // This can't be called when the slick 'init' event fires due to how slick 1696 // adds a11y features. 1697 slideshowPostInitA11ySetup.bind(this)(); 1698 1699 this.$arrows.find(selectors.arrowLeft).on('click', function() { 1700 $slideshow.slick('slickPrev'); 1701 }); 1702 this.$arrows.find(selectors.arrowRight).on('click', function() { 1703 $slideshow.slick('slickNext'); 1704 }); 1705 1706 this.$pause.on('click', this.togglePause.bind(this)); 1707 } 1708 1709 function slideshowA11ySetup(event, obj) { 1710 var $slider = obj.$slider; 1711 var $list = obj.$list; 1712 this.$dots = this.$section.find(selectors.dots); 1713 this.$mobileDots = this.$dots.eq(1); 1714 1715 // Remove default Slick aria-live attr until slider is focused 1716 $list.removeAttr('aria-live'); 1717 1718 this.$wrapper.on('keyup', keyboardNavigation.bind(this)); 1719 this.$controls.on('keyup', keyboardNavigation.bind(this)); 1720 this.$textWrapperMobile.on('keyup', keyboardNavigation.bind(this)); 1721 1722 // When an element in the slider is focused 1723 // pause slideshow and set aria-live. 1724 this.$wrapper 1725 .on( 1726 'focusin', 1727 function(evt) { 1728 if (!this.$wrapper.has(evt.target).length) { 1729 return; 1730 } 1731 1732 $list.attr('aria-live', 'polite'); 1733 if (this.autorotate) { 1734 $slider.slick('slickPause'); 1735 } 1736 }.bind(this) 1737 ) 1738 .on( 1739 'focusout', 1740 function(evt) { 1741 if (!this.$wrapper.has(evt.target).length) { 1742 return; 1743 } 1744 1745 $list.removeAttr('aria-live'); 1746 if (this.autorotate) { 1747 // Only resume playing if the user hasn't paused using the pause 1748 // button 1749 if (!this.$pause.is('.is-paused')) { 1750 $slider.slick('slickPlay'); 1751 } 1752 } 1753 }.bind(this) 1754 ); 1755 1756 // Add arrow key support when focused 1757 if (this.$dots) { 1758 this.$dots 1759 .find('a') 1760 .each(function() { 1761 var $dot = $(this); 1762 $dot.on('click keyup', function(evt) { 1763 if ( 1764 evt.type === 'keyup' && 1765 evt.which !== slate.utils.keyboardKeys.ENTER 1766 ) 1767 return; 1768 1769 evt.preventDefault(); 1770 1771 var slideNumber = $(evt.target).data('slide-number'); 1772 1773 $slider.attr('tabindex', -1).slick('slickGoTo', slideNumber); 1774 1775 if (evt.type === 'keyup') { 1776 $slider.focus(); 1777 } 1778 }); 1779 }) 1780 .eq(0) 1781 .attr('aria-current', 'true'); 1782 } 1783 1784 this.$controls 1785 .on('focusin', highlightControls.bind(this)) 1786 .on('focusout', unhighlightControls.bind(this)); 1787 } 1788 1789 function slideshowPostInitA11ySetup() { 1790 var $slides = this.$slideshow.find(selectors.slides); 1791 1792 $slides.removeAttr('role').removeAttr('aria-labelledby'); 1793 this.$dots 1794 .removeAttr('role') 1795 .find('li') 1796 .removeAttr('role') 1797 .removeAttr('aria-selected') 1798 .each(function() { 1799 var $dot = $(this); 1800 var ariaControls = $dot.attr('aria-controls'); 1801 $dot 1802 .removeAttr('aria-controls') 1803 .find('a') 1804 .attr('aria-controls', ariaControls); 1805 }); 1806 } 1807 1808 function beforeChange(event, slick, currentSlide, nextSlide) { 1809 var $dotLinks = this.$dots.find('a'); 1810 var $mobileDotLinks = this.$mobileDots.find('li'); 1811 1812 $dotLinks 1813 .removeAttr('aria-current') 1814 .eq(nextSlide) 1815 .attr('aria-current', 'true'); 1816 1817 $mobileDotLinks
1818 .removeClass(classes.slickActiveMobile) 1819 .eq(nextSlide) 1820 .addClass(classes.slickActiveMobile); 1821 this.showMobileText(nextSlide); 1822 } 1823 1824 function keyboardNavigation() { 1825 if (event.keyCode === slate.utils.keyboardKeys.LEFTARROW) { 1826 this.$slideshow.slick('slickPrev'); 1827 } 1828 if (event.keyCode === slate.utils.keyboardKeys.RIGHTARROW) { 1829 this.$slideshow.slick('slickNext'); 1830 } 1831 } 1832 1833 function highlightControls() { 1834 this.$controls.addClass(classes.controlsHover); 1835 } 1836 1837 function unhighlightControls() { 1838 this.$controls.removeClass(classes.controlsHover); 1839 } 1840 1841 slideshow.prototype.togglePause = function() { 1842 var slideshowSelector = getSlideshowId(this.$pause); 1843 if (this.$pause.hasClass(classes.isPaused)) { 1844 this.$pause.removeClass(classes.isPaused).attr('aria-pressed', 'false'); 1845 if (this.autorotate) { 1846 $(slideshowSelector).slick('slickPlay'); 1847 } 1848 } else { 1849 this.$pause.addClass(classes.isPaused).attr('aria-pressed', 'true'); 1850 if (this.autorotate) { 1851 $(slideshowSelector).slick('slickPause'); 1852 } 1853 } 1854 }; 1855 1856 slideshow.prototype.setSlideshowHeight = function() { 1857 var minAspectRatio = this.$slideshow.data('min-aspect-ratio'); 1858 this.$slideshow.height($(document).width() / minAspectRatio); 1859 }; 1860 1861 slideshow.prototype.showMobileText = function(slideIndex) { 1862 var $allTextContent = this.$textWrapperMobile.find( 1863 selectors.textContentMobile 1864 ); 1865 var currentTextContentSelector = 1866 selectors.textContentMobile + '-' + slideIndex; 1867 var $currentTextContent = this.$textWrapperMobile.find( 1868 currentTextContentSelector 1869 ); 1870 if ( 1871 !$currentTextContent.length && 1872 this.$slideshow.find(selectors.slides).length === 1 1873 ) { 1874 this.$textWrapperMobile.hide(); 1875 } else { 1876 this.$textWrapperMobile.show(); 1877 } 1878 $allTextContent.hide(); 1879 $currentTextContent.show(); 1880 }; 1881 1882 function getSlideshowId($el) { 1883 return '#Slideshow-' + $el.data('id'); 1884 } 1885 1886 return slideshow; 1887})(); 1888 1889// Youtube API callback 1890// eslint-disable-next-line no-unused-vars 1891function onYouTubeIframeAPIReady() { 1892 theme.Video.loadVideos(); 1893} 1894 1895theme.Video = (function() { 1896 var autoplayCheckComplete = false; 1897 var playOnClickChecked = false; 1898 var playOnClick = false; 1899 var youtubeLoaded = false; 1900 var videos = {}; 1901 var videoPlayers = []; 1902 var videoOptions = { 1903 ratio: 16 / 9, 1904 scrollAnimationDuration: 400, 1905 playerVars: { 1906 // eslint-disable-next-line camelcase 1907 iv_load_policy: 3, 1908 modestbranding: 1, 1909 autoplay: 0, 1910 controls: 0, 1911 wmode: 'opaque', 1912 branding: 0, 1913 autohide: 0, 1914 rel: 0 1915 }, 1916 events: { 1917 onReady: onPlayerReady, 1918 onStateChange: onPlayerChange 1919 } 1920 }; 1921 var classes = { 1922 playing: 'video-is-playing', 1923 paused: 'video-is-paused', 1924 loading: 'video-is-loading', 1925 loaded: 'video-is-loaded', 1926 backgroundVideoWrapper: 'video-background-wrapper', 1927 videoWithImage: 'video--image_with_play', 1928 backgroundVideo: 'video--background', 1929 userPaused: 'is-paused', 1930 supportsAutoplay: 'autoplay', 1931 supportsNoAutoplay: 'no-autoplay', 1932 wrapperMinHeight: 'video-section-wrapper--min-height' 1933 }; 1934 1935 var selectors = { 1936 section: '.video-section', 1937 videoWrapper: '.video-section-wrapper', 1938 playVideoBtn: '.video-control__play', 1939 closeVideoBtn: '.video-control__close-wrapper', 1940 pauseVideoBtn: '.video__pause', 1941 pauseVideoStop: '.video__pause-stop', 1942 pauseVideoResume: '.video__pause-resume', 1943 fallbackText: '.icon__fallback-text' 1944 }; 1945 1946 /** 1947 * Public functions 1948 */ 1949 function init($video) { 1950 if (!$video.length) { 1951 return; 1952 } 1953 1954 videos[$video.attr('id')] = { 1955 id: $video.attr('id'), 1956 videoId: $video.data('id'), 1957 type: $video.data('type'), 1958 status: 1959 $video.data('type') === 'image_with_play' ? 'closed' : 'background', // closed, open, background 1960 $video: $video, 1961 $videoWrapper: $video.closest(selectors.videoWrapper), 1962 $section: $video.closest(selectors.section), 1963 controls: $video.data('type') === 'background' ? 0 : 1 1964 }; 1965 1966 if (!youtubeLoaded) { 1967 // This code loads the IFrame Player API code asynchronously. 1968 var tag = document.createElement('script'); 1969 tag.src = 'https://www.youtube.com/iframe_api'; 1970 var firstScriptTag = document.getElementsByTagName('script')[0]; 1971 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 1972 } 1973 1974 playOnClickCheck(); 1975 } 1976 1977 function customPlayVideo(playerId) { 1978 // Make sure we have carried out the playOnClick check first 1979 if (!playOnClickChecked && !playOnClick) { 1980 return; 1981 } 1982 1983 if (playerId && typeof videoPlayers[playerId].playVideo === 'function') { 1984 privatePlayVideo(playerId); 1985 } 1986 } 1987 1988 function pauseVideo(playerId) { 1989 if ( 1990 videoPlayers[playerId] && 1991 typeof videoPlayers[playerId].pauseVideo === 'function' 1992 ) { 1993 videoPlayers[playerId].pauseVideo(); 1994 } 1995 } 1996 1997 function loadVideos() { 1998 for (var key in videos) { 1999 if (videos.hasOwnProperty(key)) { 2000 createPlayer(key); 2001 } 2002 } 2003 2004 initEvents(); 2005 youtubeLoaded = true; 2006 } 2007 2008 function editorLoadVideo(key) { 2009 if (!youtubeLoaded) { 2010 return; 2011 } 2012 createPlayer(key); 2013 2014 initEvents(); 2015 } 2016 2017 /** 2018 * Private functions 2019 */ 2020 2021 function privatePlayVideo(id, clicked) { 2022 var videoData = videos[id]; 2023 var player = videoPlayers[id]; 2024 var $videoWrapper = videoData.$videoWrapper; 2025 2026 if (playOnClick) { 2027 // playOnClick means we are probably on mobile (no autoplay). 2028 // setAsPlaying will show the iframe, requiring another click 2029 // to play the video. 2030 setAsPlaying(videoData); 2031 } else if (clicked || autoplayCheckComplete) { 2032 // Play if autoplay is available or clicked to play
2033 $videoWrapper.removeClass(classes.loading); 2034 setAsPlaying(videoData); 2035 player.playVideo(); 2036 return; 2037 } else { 2038 player.playVideo(); 2039 } 2040 } 2041 2042 function setAutoplaySupport(supported) { 2043 var supportClass = supported 2044 ? classes.supportsAutoplay 2045 : classes.supportsNoAutoplay; 2046 $(document.documentElement) 2047 .removeClass(classes.supportsAutoplay) 2048 .removeClass(classes.supportsNoAutoplay) 2049 .addClass(supportClass); 2050 2051 if (!supported) { 2052 playOnClick = true; 2053 } 2054 2055 autoplayCheckComplete = true; 2056 } 2057 2058 function playOnClickCheck() { 2059 // Bail early for a few instances: 2060 // - small screen 2061 // - device sniff mobile browser 2062 2063 if (playOnClickChecked) { 2064 return; 2065 } 2066 2067 if (isMobile()) { 2068 playOnClick = true; 2069 } 2070 2071 if (playOnClick) { 2072 // No need to also do the autoplay check 2073 setAutoplaySupport(false); 2074 } 2075 2076 playOnClickChecked = true; 2077 } 2078 2079 // The API will call this function when each video player is ready 2080 function onPlayerReady(evt) { 2081 evt.target.setPlaybackQuality('hd1080'); 2082 var videoData = getVideoOptions(evt); 2083 var videoTitle = evt.target.getVideoData().title; 2084 playOnClickCheck(); 2085 2086 // Prevent tabbing through YouTube player controls until visible 2087 $('#' + videoData.id).attr('tabindex', '-1'); 2088 2089 sizeBackgroundVideos(); 2090 setButtonLabels(videoData.$videoWrapper, videoTitle); 2091 2092 // Customize based on options from the video ID 2093 if (videoData.type === 'background') { 2094 evt.target.mute(); 2095 privatePlayVideo(videoData.id); 2096 } 2097
2098 videoData.$videoWrapper.addClass(classes.loaded); 2099 } 2100 2101 function onPlayerChange(evt) { 2102 var videoData = getVideoOptions(evt); 2103 if ( 2104 videoData.status === 'background' && 2105 !isMobile() && 2106 !autoplayCheckComplete && 2107 (evt.data === YT.PlayerState.PLAYING || 2108 evt.data === YT.PlayerState.BUFFERING) 2109 ) { 2110 setAutoplaySupport(true); 2111 autoplayCheckComplete = true; 2112 videoData.$videoWrapper.removeClass(classes.loading); 2113 } 2114 switch (evt.data) { 2115 case YT.PlayerState.ENDED: 2116 setAsFinished(videoData); 2117 break; 2118 case YT.PlayerState.PAUSED: 2119 // Seeking on a YouTube video also fires a PAUSED state change, 2120 // checking the state after a delay prevents us pausing the video when 2121 // the user is seeking instead of pausing 2122 setTimeout(function() { 2123 if (evt.target.getPlayerState() === YT.PlayerState.PAUSED) { 2124 setAsPaused(videoData); 2125 } 2126 }, 200); 2127 break; 2128 } 2129 } 2130 2131 function setAsFinished(videoData) { 2132 switch (videoData.type) { 2133 case 'background': 2134 videoPlayers[videoData.id].seekTo(0); 2135 break; 2136 case 'image_with_play': 2137 closeVideo(videoData.id); 2138 toggleExpandVideo(videoData.id, false); 2139 break; 2140 } 2141 } 2142 2143 function setAsPlaying(videoData) { 2144 var $videoWrapper = videoData.$videoWrapper; 2145 var $pauseButton = $videoWrapper.find(selectors.pauseVideoBtn); 2146 2147 $videoWrapper.removeClass(classes.loading); 2148
2149 if ($pauseButton.hasClass(classes.userPaused)) { 2150 $pauseButton.removeClass(classes.userPaused); 2151 } 2152 2153 // Do not change element visibility if it is a background video 2154 if (videoData.status === 'background') { 2155 return; 2156 } 2157 2158 $('#' + videoData.id).attr('tabindex', '0'); 2159 2160 if (videoData.type === 'image_with_play') { 2161 $videoWrapper.removeClass(classes.paused).addClass(classes.playing); 2162 } 2163 2164 // Update focus to the close button so we stay within the video wrapper, 2165 // allowing time for the scroll animation 2166 setTimeout(function() { 2167 $videoWrapper.find(selectors.closeVideoBtn).focus(); 2168 }, videoOptions.scrollAnimationDuration); 2169 } 2170 2171 function setAsPaused(videoData) { 2172 var $videoWrapper = videoData.$videoWrapper; 2173 2174 // YT's events fire after our click event. This status flag ensures 2175 // we don't interact with a closed or background video. 2176 if (videoData.type === 'image_with_play') { 2177 if (videoData.status === 'closed') { 2178 $videoWrapper.removeClass(classe
2178s.paused); 2179 } else { 2180 $videoWrapper.addClass(classes.paused); 2181 } 2182 } 2183 2184 $videoWrapper.removeClass(classes.playing); 2185 } 2186 2187 function closeVideo(playerId) { 2188 var videoData = videos[playerId]; 2189 var $videoWrapper = videoData.$videoWrapper; 2190 var classesToRemove = [classes.paused, classes.playing].join(' '); 2191 2192 if (isMobile()) { 2193 $videoWrapper.removeAttr('style'); 2194 } 2195 2196 $('#' + videoData.id).attr('tabindex', '-1'); 2197 2198 videoData.status = 'closed'; 2199 2200 switch (videoData.type) { 2201 case 'image_with_play': 2202 videoPlayers[playerId].stopVideo(); 2203 setAsPaused(videoData); // in case the video is already paused 2204 break; 2205 case 'background': 2206 videoPlayers[playerId].mute(); 2207 setBackgroundVideo(playerId); 2208 break; 2209 } 2210 2211 $videoWrapper.removeClass(classesToRemove); 2212 } 2213 2214 function getVideoOptions(evt) { 2215 return videos[evt.target.a.id]; 2216 } 2217 2218 function toggleExpandVideo(playerId, expand) { 2219 var video = videos[playerId]; 2220 var elementTop = video.$videoWrapper.offset().top; 2221 var $playButton = video.$videoWrapper.find(selectors.playVideoBtn); 2222 var offset = 0; 2223 var newHeight = 0; 2224 2225 if (isMobile()) { 2226 video.$videoWrapper.parent().toggleClass('page-width', !expand); 2227 } 2228 2229 if (expand) { 2230 if (isMobile()) { 2231 newHeight = $(window).width() / videoOptions.ratio; 2232 } else { 2233 newHeight = video.$videoWrapper.width() / videoOptions.ratio; 2234 } 2235 offset = ($(window).height() - newHeight) / 2; 2236 2237 video.$videoWrapper 2238 .removeClass(classes.wrapperMinHeight) 2239 .animate({ height: newHeight }, 600); 2240 2241 // Animate doesn't work in mobile editor, so we don't use it 2242 if (!(isMobile() && Shopify.designMode)) { 2243 $('html, body').animate( 2244 { 2245 scrollTop: elementTop - offset 2246 }, 2247 videoOptions.scrollAnimationDuration 2248 ); 2249 } 2250 } else { 2251 if (isMobile()) { 2252 newHeight = video.$videoWrapper.data('mobile-height'); 2253 } else { 2254 newHeight = video.$videoWrapper.data('desktop-height'); 2255 } 2256 2257 video.$videoWrapper 2258 .height(video.$videoWrapper.width() / videoOptions.ratio) 2259 .animate({ height: newHeight }, 600); 2260 setTimeout(function() {
2261 video.$videoWrapper.addClass(classes.wrapperMinHeight); 2262 }, 600); 2263 $playButton.focus(); 2264 } 2265 } 2266 2267 function togglePause(playerId) { 2268 var $pauseButton = videos[playerId].$videoWrapper.find( 2269 selectors.pauseVideoBtn 2270 ); 2271 var paused = $pauseButton.hasClass(classes.userPaused); 2272 if (paused) { 2273 $pauseButton.removeClass(classes.userPaused); 2274 customPlayVideo(playerId); 2275 } else { 2276 $pauseButton.addClass(classes.userPaused); 2277 pauseVideo(playerId); 2278 } 2279 $pauseButton.attr('aria-pressed', !paused); 2280 } 2281 2282 function startVideoOnClick(playerId) { 2283 var video = videos[playerId]; 2284 2285 // add loading class to wrapper 2286 video.$videoWrapper.addClass(classes.loading); 2287 2288 // Explicity set the video wrapper height (needed for height transition) 2289 video.$videoWrapper.attr( 2290 'style', 2291 'height: ' + video.$videoWrapper.height() + 'px' 2292 ); 2293 2294 video.status = 'open'; 2295 2296 switch (video.type) { 2297 case 'image_with_play': 2298 privatePlayVideo(playerId, true); 2299 break; 2300 case 'background': 2301 unsetBackgroundVideo(playerId, video); 2302 videoPlayers[playerId].unMute(); 2303 privatePlayVideo(playerId, true); 2304 break; 2305 } 2306 2307 toggleExpandVideo(playerId, true); 2308 2309 // esc to close video player 2310 $(document).on('keydown.videoPlayer', function(evt) { 2311 var playerId = $(document.activeElement).data('controls'); 2312 if (evt.keyCode !== slate.utils.keyboardKeys.ESCAPE || !playerId) { 2313 return; 2314 } 2315 2316 closeVideo(playerId); 2317 toggleExpandVideo(playerId, false); 2318 }); 2319 } 2320 2321 function sizeBackgroundVideos() { 2322 $('.' + classes.backgroundVideo).each(function(index, el) { 2323 sizeBackgroundVideo($(el)); 2324 }); 2325 } 2326 2327 function sizeBackgroundVideo($videoPlayer) { 2328 if (!youtubeLoaded) { 2329 return; 2330 } 2331 2332 if (isMobile()) { 2333 $videoPlayer.removeAttr('style'); 2334 } else { 2335 var $videoWrapper = $videoPlayer.closest(selectors.videoWrapper); 2336 var videoWidth = $videoWrapper.width(); 2337 var playerWidth = $videoPlayer.width(); 2338 var desktopHeight = $videoWrapper.data('desktop-height'); 2339 2340 // when screen aspect ratio differs from video, video must center and underlay one dimension 2341 if (videoWidth / videoOptions.ratio < desktopHeight) { 2342 playerWidth = Math.ceil(desktopHeight * videoOptions.ratio); // get new player width 2343 $videoPlayer 2344 .width(playerWidth) 2345 .height(desktopHeight) 2346 .css({ 2347 left: (videoWidth - playerWidth) / 2, 2348 top: 0 2349 }); // player width is greater, offset left; reset top 2350 } else { 2351 // new video width < window width (gap to right) 2352 desktopHeight = Math.ceil(videoWidth / videoOptions.ratio); // get new player height 2353 $videoPlayer 2354 .width(videoWidth) 2355 .height(desktopHeight) 2356 .css({ 2357 left: 0, 2358 top: (desktopHeight - desktopHeight) / 2 2359 }); // player height is greater, offset top; reset left 2360 } 2361 2362 $videoPlayer.prepareTransition();
2363 $videoWrapper.addClass(classes.loaded); 2364 } 2365 } 2366 2367 function unsetBackgroundVideo(playerId) { 2368 // Switch the background video to a chrome-only player once played 2369 $('#' + playerId) 2370 .removeClass(classes.backgroundVideo) 2371 .addClass(classes.videoWithImage); 2372 2373 setTimeout(function() { 2374 $('#' + playerId).removeAttr('style'); 2375 }, 600); 2376 2377 videos[playerId].$videoWrapper 2378 .removeClass(classes.backgroundVideoWrapper) 2379 .addClass(classes.playing); 2380 2381 videos[playerId].status = 'open'; 2382 } 2383 2384 function setBackgroundVideo(playerId) { 2385 $('#' + playerId) 2386 .removeClass(classes.videoWithImage) 2387 .addClass(classes.backgroundVideo); 2388 2389 videos[playerId].$videoWrapper.addClass(classes.backgroundVideoWrapper); 2390 2391 videos[playerId].status = 'background'; 2392 sizeBackgroundVideo($('#' + playerId)); 2393 } 2394 2395 function isMobile() { 2396 return $(window).width() < 750 || window.mobileCheck(); 2397 } 2398 2399 function initEvents() { 2400 $(document).on('click.videoPlayer', selectors.playVideoBtn, function(evt) { 2401 var playerId = $(evt.currentTarget).data('controls'); 2402 2403 startVideoOnClick(playerId); 2404 }); 2405 2406 $(document).on('click.videoPlayer', selectors.closeVideoBtn, function(evt) { 2407 var playerId = $(evt.currentTarget).data('controls'); 2408 2409 $(evt.currentTarget).blur(); 2410 closeVideo(playerId); 2411 toggleExpandVideo(playerId, false); 2412 }); 2413 2414 $(document).on('click.videoPlayer', selectors.pauseVideoBtn, function(evt) { 2415 var playerId = $(evt.currentTarget).data('controls'); 2416 togglePause(playerId); 2417 }); 2418 2419 // Listen to resize to keep a background-size:cover-like layout 2420 $(window).on( 2421 'resize.videoPlayer', 2422 $.debounce(200, function() { 2423 if (!youtubeLoaded) return; 2424 var key; 2425 var fullscreen = window.innerHeight === screen.height; 2426 2427 sizeBackgroundVideos(); 2428 2429 if (isMobile()) { 2430 for (key in videos) { 2431 if (videos.hasOwnProperty(key)) {
2432 if (videos[key].$videoWrapper.hasClass(classes.playing)) { 2433 if (!fullscreen) { 2434 pauseVideo(key); 2435 setAsPaused(videos[key]); 2436 } 2437 } 2438 videos[key].$videoWrapper.height( 2439 $(document).width() / videoOptions.ratio 2440 ); 2441 } 2442 } 2443 setAutoplaySupport(false); 2444 } else { 2445 setAutoplaySupport(true); 2446 for (key in videos) { 2447 if ( 2448 videos[key].$videoWrapper.find('.' + classes.videoWithImage) 2449 .length 2450 ) { 2451 continue; 2452 } 2453 videoPlayers[key].playVideo(); 2454 setAsPlaying(videos[key]); 2455 } 2456 } 2457 }) 2458 ); 2459 2460 $(window).on( 2461 'scroll.videoPlayer', 2462 $.debounce(50, function() { 2463 if (!youtubeLoaded) return; 2464 2465 for (var key in videos) { 2466 if (videos.hasOwnProperty(key)) { 2467 var $videoWrapper = videos[key].$videoWrapper; 2468 2469 // Close the video if more than 75% of it is scrolled out of view 2470 if (
2471 $videoWrapper.hasClass(classes.playing) && 2472 ($videoWrapper.offset().top + $videoWrapper.height() * 0.75 < 2473 $(window).scrollTop() || 2474 $videoWrapper.offset().top + $videoWrapper.height() * 0.25 > 2475 $(window).scrollTop() + $(window).height()) 2476 ) { 2477 closeVideo(key); 2478 toggleExpandVideo(key, false); 2479 } 2480 } 2481 } 2482 }) 2483 ); 2484 } 2485 2486 function createPlayer(key) { 2487 var args = $.extend({}, videoOptions, videos[key]); 2488 args.playerVars.controls = args.controls; 2489 videoPlayers[key] = new YT.Player(key, args); 2490 } 2491 2492 function removeEvents() { 2493 $(document).off('.videoPlayer'); 2494 $(window).off('.videoPlayer'); 2495 } 2496 2497 function setButtonLabels($videoWrapper, title) { 2498 var $playButtons = $videoWrapper.find(selectors.playVideoBtn); 2499 var $closeButton = $videoWrapper.find(selectors.closeVideoBtn); 2500 var $pauseButton = $videoWrapper.find(selectors.pauseVideoBtn); 2501 var $closeButtonText = $closeButton.find(selectors.fallbackText); 2502 var $pauseButtonStopText = $pauseButton 2503 .find(selectors.pauseVideoStop) 2504 .find(selectors.fallbackText); 2505 var $pauseButtonResumeText = $pauseButton 2506 .find(selectors.pauseVideoResume) 2507 .find(selectors.fallbackText); 2508 2509 // Insert the video title retrieved from YouTube into the instructional text 2510 // for each button 2511 $playButtons.each(function() { 2512 var $playButton = $(this); 2513 var $playButtonText = $playButton.find(selectors.fallbackText); 2514 2515 $playButtonText.text( 2516 $playButtonText.text().replace('[video_title]', title) 2517 ); 2518 }); 2519 $closeButtonText.text( 2520 $closeButtonText.text().replace('[video_title]', title) 2521 ); 2522 $pauseButtonStopText.text( 2523 $pauseButtonStopText.text().replace('[video_title]', title) 2524 ); 2525 $pauseButtonResumeText.text( 2526 $pauseButtonResumeText.text().replace('[video_title]', title) 2527 ); 2528 } 2529 2530 return { 2531 init: init, 2532 editorLoadVideo: editorLoadVideo, 2533 loadVideos: loadVideos, 2534 playVideo: customPlayVideo, 2535 pauseVideo: pauseVideo, 2536 removeEvents: removeEvents 2537 }; 2538})(); 2539 2540window.theme = window.theme || {}; 2541 2542theme.FormStatus = (function() { 2543 var selectors = { 2544 statusMessage: '[data-form-status]' 2545 }; 2546 2547 function init() { 2548 this.$statusMessage = $(selectors.statusMessage); 2549 2550 if (!this.$statusMessage) return; 2551 2552 this.$statusMessage.attr('tabindex', -1).focus(); 2553 2554 this.$statusMessage.on('blur', handleBlur.bind(this)); 2555 } 2556 2557 function handleBlur() { 2558 this.$statusMessage.removeAttr('tabindex'); 2559 } 2560 2561 return { 2562 init: init 2563 }; 2564})(); 2565 2566theme.Hero = (function() { 2567 var classes = { 2568 indexSectionFlush: 'index-section--flush' 2569 }; 2570 2571 var selectors = { 2572 heroFixedWidthContent: '.hero-fixed-width__content', 2573 heroFixedWidthImage: '.hero-fixed-width__image' 2574 }; 2575 2576 function hero(el, sectionId) { 2577 this.$hero = $(el); 2578 this.layout = this.$hero.data('layout'); 2579 var $parentSection = $('#shopify-section-' + sectionId); 2580 var $heroContent = $parentSection.find(selectors.heroFixedWidthContent); 2581 var $heroImage = $parentSection.find(selectors.heroFixedWidthImage); 2582 2583 if (this.layout !== 'fixed_width') { 2584 return; 2585 } 2586 2587 $parentSection.removeClass(classes.indexSectionFlush); 2588 heroFixedHeight(); 2589 $(window).resize( 2590 $.debounce(50, function() { 2591 heroFixedHeight(); 2592 }) 2593 ); 2594 2595 function heroFixedHeight() { 2596 var contentHeight = $heroContent.height() + 50; 2597 var imageHeight = $heroImage.height(); 2598 2599 if (contentHeight > imageHeight) { 2600 $heroImage.css('min-height', contentHeight); 2601 } 2602 } 2603 } 2604 2605 return hero; 2606})(); 2607 2608 2609/* ================ TEMPLATES ================ */ 2610(function() { 2611 var $filterBy = $('#BlogTagFilter'); 2612 2613 if (!$filterBy.length) { 2614 return; 2615 } 2616 2617 $filterBy.on('change', function() { 2618 location.href = $(this).val(); 2619 }); 2620})(); 2621 2622window.theme = theme || {}; 2623 2624theme.customerTemplates = (function() { 2625 var selectors = { 2626 RecoverHeading: '#RecoverHeading', 2627 RecoverEmail: '#RecoverEmail', 2628 LoginHeading: '#LoginHeading' 2629 }; 2630 2631 function initEventListeners() { 2632 this.$RecoverHeading = $(selectors.RecoverHeading); 2633 this.$RecoverEmail = $(selectors.RecoverEmail); 2634 this.$LoginHeading = $(selectors.LoginHeading); 2635 2636 // Show reset password form 2637 $('#RecoverPassword').on( 2638 'click', 2639 function(evt) { 2640 evt.preventDefault(); 2641 showRecoverPasswordForm(); 2642 this.$RecoverHeading.attr('tabindex', '-1').focus(); 2643 }.bind(this) 2644 ); 2645 2646 // Hide reset password form 2647 $('#HideRecoverPasswordLink').on( 2648 'click', 2649 function(evt) { 2650 evt.preventDefault(); 2651 hideRecoverPasswordForm(); 2652 this.$LoginHeading.attr('tabindex', '-1').focus(); 2653 }.bind(this) 2654 ); 2655 2656 this.$RecoverHeading.on('blur', function() { 2657 $(this).removeAttr('tabindex'); 2658 }); 2659 2660 this.$LoginHeading.on('blur', function() { 2661 $(this).removeAttr('tabindex'); 2662 }); 2663 } 2664 2665 /** 2666 * 2667 * Show/Hide recover password form 2668 * 2669 */ 2670 2671 function showRecoverPasswordForm() { 2672 $('#RecoverPasswordForm').removeClass('hide'); 2673 $('#CustomerLoginForm').addClass('hide'); 2674 2675 if (this.$RecoverEmail.attr('aria-invalid') === 'true') { 2676 this.$RecoverEmail.focus(); 2677 } 2678 } 2679 2680 function hideRecoverPasswordForm() { 2681 $('#RecoverPasswordForm').addClass('hide'); 2682 $('#CustomerLoginForm').removeClass('hide'); 2683 } 2684 2685 /** 2686 * 2687 * Show reset password success message 2688 * 2689 */ 2690 function resetPasswordSuccess() { 2691 var $formState = $('.reset-password-success'); 2692 2693 // check if reset password form was successfully submited. 2694 if (!$formState.length) { 2695 return; 2696 } 2697 2698 // show success message 2699 $('#ResetSuccess') 2700 .removeClass('hide') 2701 .focus(); 2702 } 2703 2704 /** 2705 * 2706 * Show/hide customer address forms 2707 * 2708 */ 2709 function customerAddressForm() { 2710 var $newAddressForm = $('#AddressNewForm'); 2711 var $newAddressFormButton = $('#AddressNewButton'); 2712 2713 if (!$newAddressForm.length) { 2714 return; 2715 } 2716 2717 // Initialize observers on address selectors, defined in shopify_common.js 2718 if (Shopify) { 2719 // eslint-disable-next-line no-new 2720 new Shopify.CountryProvinceSelector( 2721 'AddressCountryNew', 2722 'AddressProvinceNew', 2723 { 2724 hideElement: 'AddressProvinceContainerNew' 2725 } 2726 ); 2727 } 2728
2729 // Initialize each edit form's country/province selector 2730 $('.address-country-option').each(function() { 2731 var formId = $(this).data('form-id'); 2732 var countrySelector = 'AddressCountry_' + formId; 2733 var provinceSelector = 'AddressProvince_' + formId; 2734 var containerSelector = 'AddressProvinceContainer_' + formId; 2735 2736 // eslint-disable-next-line no-new 2737 new Shopify.CountryProvinceSelector(countrySelector, provinceSelector, { 2738 hideElement: containerSelector 2739 }); 2740 }); 2741 2742 // Toggle new/edit address forms 2743 $('.address-new-toggle').on('click', function() { 2744 var isExpanded = $newAddressFormButton.attr('aria-expanded') === 'true'; 2745 2746 $newAddressForm.toggleClass('hide'); 2747 $newAddressFormButton.attr('aria-expanded', !isExpanded).focus(); 2748 }); 2749 2750 $('.address-edit-toggle').on('click', function() { 2751 var formId = $(this).data('form-id'); 2752 var $editButton = $('#EditFormButton_' + formId); 2753 var $editAddress = $('#EditAddress_' + formId); 2754 var isExpanded = $editButton.attr('aria-expanded') === 'true'; 2755 2756 $editAddress.toggleClass('hide'); 2757 $editButton.attr('aria-expanded', !isExpanded).focus(); 2758 }); 2759 2760 $('.address-delete').on('click', function() { 2761 var $el = $(this); 2762 var target = $el.data('target'); 2763 var confirmMessage = $el.data('confirm-message'); 2764 2765 // eslint-disable-next-line no-alert 2766 if ( 2767 confirm( 2768 confirmMessage || 'Are you sure you wish to delete this address?' 2769 ) 2770 ) { 2771 Shopify.postLink(target, { 2772 parameters: { _method: 'delete' } 2773 }); 2774 } 2775 }); 2776 } 2777 2778 /** 2779 * 2780 * Check URL for reset password hash 2781 * 2782 */ 2783 function checkUrlHash() { 2784 var hash = window.location.hash; 2785 2786 // Allow deep linking to recover password form 2787 if (hash === '#recover') { 2788 showRecoverPasswordForm.bind(this)(); 2789 } 2790 } 2791 2792 return { 2793 init: function() { 2794 initEventListeners(); 2795 checkUrlHash(); 2796 resetPasswordSuccess(); 2797 customerAddressForm(); 2798 } 2799 }; 2800})(); 2801 2802 2803/*================ SECTIONS ================*/ 2804window.theme = window.theme || {}; 2805 2806theme.Cart = (function() { 2807 var selectors = { 2808 cartCount: '[data-cart-count]', 2809 cartCountBubble: '[data-cart-count-bubble]', 2810 cartDiscount: '[data-cart-discount]', 2811 cartDiscountTitle: '[data-cart-discount-title]', 2812 cartDiscountAmount: '[data-cart-discount-amount]', 2813 cartDiscountWrapper: '[data-cart-discount-wrapper]', 2814 cartErrorMessage: '[data-cart-error-message]', 2815 cartErrorMessageWrapper: '[data-cart-error-message-wrapper]', 2816 cartItem: '[data-cart-item]', 2817 cartItemDetails: '[data-cart-item-details]', 2818 cartItemDiscount: '[data-cart-item-discount]', 2819 cartItemDiscountedPriceGroup: '[data-cart-item-discounted-price-group]', 2820 cartItemDiscountTitle: '[data-cart-item-discount-title]', 2821 cartItemDiscountAmount: '[data-cart-item-discount-amount]', 2822 cartItemDiscountList: '[data-cart-item-discount-list]', 2823 cartItemFinalPrice: '[data-cart-item-final-price]', 2824 cartItemImage: '[data-cart-item-image]', 2825 cartItemLinePrice: '[data-cart-item-line-price]', 2826 cartItemOriginalPrice: '[data-cart-item-original-price]', 2827 cartItemPrice: '[data-cart-item-price]', 2828 cartItemPriceList: '[data-cart-item-price-list]', 2829 cartItemProperty: '[data-cart-item-property]', 2830 cartItemPropertyName: '[data-cart-item-property-name]', 2831 cartItemPropertyValue: '[data-cart-item-property-value]', 2832 cartItemRegularPriceGroup: '[data-cart-item-regular-price-group]', 2833 cartItemRegularPrice: '[data-cart-item-regular-price]', 2834 cartItemTitle: '[data-cart-item-title]', 2835 cartItemOption: '[data-cart-item-option]', 2836 cartLineItems: '[data-cart-line-items]', 2837 cartNote: '[data-cart-notes]', 2838 cartQuantityErrorMessage: '[data-cart-quantity-error-message]', 2839 cartQuantityErrorMessageWrapper: 2840 '[data-cart-quantity-error-message-wrapper]', 2841 cartRemove: '[data-cart-remove]', 2842 cartStatus: '[data-cart-status]', 2843 cartSubtotal: '[data-cart-subtotal]', 2844 cartTableCell: '[data-cart-table-cell]', 2845 cartWrapper: '[data-cart-wrapper]', 2846 emptyPageContent: '[data-empty-page-content]', 2847 quantityInput: '[data-quantity-input]', 2848 quantityInputMobile: '[data-quantity-input-mobile]', 2849 quantityInputDesktop: '[data-quantity-input-desktop]', 2850 quantityLabelMobile: '[data-quantity-label-mobile]', 2851 quantityLabelDesktop: '[data-quantity-label-desktop]', 2852 inputQty: '[data-quantity-input]', 2853 thumbnails: '.cart__image', 2854 unitPrice: '[data-unit-price]', 2855 unitPriceBaseUnit: '[data-unit-price-base-unit]', 2856 unitPriceGroup: '[data-unit-price-group]' 2857 }; 2858 2859 var classes = {
2860 cartNoCookies: 'cart--no-cookies', 2861 cartRemovedProduct: 'cart__removed-product', 2862 hide: 'hide', 2863 inputError: 'input--error' 2864 }; 2865 2866 var attributes = { 2867 cartItemIndex: 'data-cart-item-index', 2868 cartItemKey: 'data-cart-item-key', 2869 cartItemQuantity: 'data-cart-item-quantity', 2870 cartItemTitle: 'data-cart-item-title', 2871 cartItemUrl: 'data-cart-item-url', 2872 quantityItem: 'data-quantity-item' 2873 }; 2874 2875 var mediumUpQuery = '(min-width: ' + theme.breakpoints.medium + 'px)'; 2876 2877 function Cart(container) { 2878 this.$container = $(container); 2879 this.$thumbnails = $(selectors.thumbnails, this.$container); 2880 this.ajaxEnabled = this.$container.data('ajax-enabled'); 2881 2882 if (!this.cookiesEnabled()) { 2883 this.$container.addClass(classes.cartNoCookies); 2884 } 2885 2886 this.$thumbnails.css('cursor', 'pointer'); 2887 this.$container.on( 2888 'click', 2889 selectors.thumbnails, 2890 this._handleThumbnailClick 2891 ); 2892 2893 this.$container.on( 2894 'change', 2895 selectors.inputQty, 2896 $.debounce(500, this._handleInputQty.bind(this)) 2897 ); 2898 2899 this.mql = window.matchMedia(mediumUpQuery); 2900 this.mql.addListener(this.setQuantityFormControllers.bind(this)); 2901 this.setQuantityFormControllers(); 2902 2903 if (this.ajaxEnabled) { 2904 /** 2905 * Because the entire cart is recreated when a cart item is updated, 2906 * we cannot cache the elements in the cart. Instead, we add the event 2907 * listeners on the cart's container to allow us to retain the event 2908 * listeners after rebuilding the cart when an item is updated. 2909 */ 2910 2911 this.$container.on( 2912 'change', 2913 selectors.cartNote, 2914 this._onNoteChange.bind(this) 2915 ); 2916 2917 this.$container.on( 2918 'click', 2919 selectors.cartRemove, 2920 this._onRemoveItem.bind(this) 2921 ); 2922 2923 this._setupCartTemplates(); 2924 } 2925 } 2926 2927 Cart.prototype = _.assignIn({}
2927, Cart.prototype, { 2928 _setupCartTemplates: function() { 2929 this.$itemTemplate = $(selectors.cartItem, this.$container) 2930 .first() 2931 .clone(); 2932 this.$itemDiscountTemplate = $( 2933 selectors.cartItemDiscount, 2934 this.$itemTemplate 2935 ).clone(); 2936 this.$itemOptionTemplate = $( 2937 selectors.cartItemOption, 2938 this.$itemTemplate 2939 ).clone(); 2940 this.$itemPropertyTemplate = $( 2941 selectors.cartItemProperty, 2942 this.$itemTemplate 2943 ).clone(); 2944 this.$itemPriceListTemplate = $( 2945 selectors.cartItemPriceList, 2946 this.$itemTemplate 2947 ).clone(); 2948 this.$itemLinePriceTemplate = $( 2949 selectors.cartItemLinePrice, 2950 this.$itemTemplate 2951 ).clone(); 2952 this.$cartDiscountTemplate = $( 2953 selectors.cartDiscount, 2954 this.$container 2955 ).clone(); 2956 }, 2957 2958 _handleInputQty: function(evt) { 2959 var $input = $(evt.target); 2960 var itemIndex = $input.data('quantity-item'); 2961 var $itemElement = $input.closest(selectors.cartItem); 2962 var $itemQtyInputs = $('[data-quantity-item=' + itemIndex + ']'); 2963 var value = parseInt($input.val()); 2964 var isValidValue = !(value < 0 || isNaN(value)); 2965 $itemQtyInputs.val(value); 2966 2967 this._hideCartError(); 2968 this._hideQuantityErrorMessage(); 2969 2970 if (!isValidValue) { 2971 this._showQuantityErrorMessages($itemElement); 2972 return; 2973 } 2974 2975 if (isValidValue && this.ajaxEnabled) { 2976 this._updateItemQuantity( 2977 itemIndex, 2978 $itemElement, 2979 $itemQtyInputs, 2980 value 2981 ); 2982 } 2983 }, 2984 2985 _updateItemQuantity: function( 2986 itemIndex, 2987 $itemElement, 2988 $itemQtyInputs, 2989 value 2990 ) { 2991 var key = $itemElement.attr(attributes.cartItemKey); 2992 var index = $itemElement.attr(attributes.cartItemIndex); 2993 2994 var params = { 2995 url: '/cart/change.js', 2996 data: { quantity: value, line: index }, 2997 dataType: 'json' 2998 }; 2999 3000 $.post(params) 3001 .done( 3002 function(state) { 3003 if (state.item_count === 0) { 3004 this._emptyCart(); 3005 } else { 3006 this._createCart(state); 3007 3008 if (value === 0) { 3009 this._showRemoveMessage($itemElement.clone()); 3010 } else { 3011 var $lineItem = $('[data-cart-item-key="' + key + '"]'); 3012 var item = this.getItem(key, state); 3013 3014 $(selectors.quantityInput, $lineItem).focus(); 3015 this._updateLiveRegion(item); 3016 } 3017 } 3018 3019 this._setCartCountBubble(state.item_count); 3020 }.bind(this) 3021 ) 3022 .fail( 3023 function() { 3024 this._showCartError($itemQtyInputs); 3025 }.bind(this) 3026 ); 3027 }, 3028 3029 getItem: function(key, state) { 3030 return state.items.find(function(item) { 3031 return item.key === key; 3032 }); 3033 }, 3034 3035 _liveRegionText: function(item) { 3036 // Dummy content for live region 3037 var liveRegionText = 3038 theme.strings.update + 3039 ': [QuantityLabel]: [Quantity], [Regular] [$$] [DiscountedPrice] [$]. [PriceInformation]'; 3040 3041 // Update Quantity 3042 liveRegionText = liveRegionText 3043 .replace('[QuantityLabel]', theme.strings.quantity) 3044 .replace('[Quantity]', item.quantity); 3045 3046 // Update pricing information 3047 var regularLabel = ''; 3048 var regularPrice = theme.Currency.formatMoney( 3049 item.original_line_price, 3050 theme.moneyFormat 3051 ); 3052 var discountLabel = ''; 3053 var discountPrice = ''; 3054 var discountInformation = ''; 3055 3056 if (item.original_line_price > item.final_line_price) { 3057 regularLabel = theme.strings.regularTotal; 3058 3059 discountLabel = theme.strings.discountedTotal; 3060 discountPrice = theme.Currency.formatMoney( 3061 item.final_line_price, 3062 theme.moneyFormat 3063 ); 3064 3065 discountInformation = theme.strings.priceColumn; 3066 } 3067 3068 liveRegionText = liveRegionText 3069 .replace('[Regular]', regularLabel) 3070 .replace('[$$]', regularPrice) 3071 .replace('[DiscountedPrice]', discountLabel) 3072 .replace('[$]', discountPrice) 3073 .replace('[PriceInformation]', discountInformation) 3074 .trim(); 3075 3076 return liveRegionText; 3077 }, 3078 3079 _updateLiveRegion: function(item) { 3080 var $liveRegion = $(selectors.cartStatus); 3081 $liveRegion.html(this._liveRegionText(item)).attr('aria-hidden', false); 3082 3083 // hide content from accessibility tree after announcement 3084 setTimeout(function() { 3085 $liveRegion.attr('aria-hidden', true); 3086 }, 1000); 3087 }, 3088 3089 _createCart: function(state) { 3090 var cartDiscountList = this._createCartDiscountList(state); 3091 3092 $(selectors.cartLineItems, this.$container).html( 3093 this._createLineItemList(state) 3094 ); 3095 3096 this.setQuantityFormControllers(); 3097 3098 $(selectors.cartNote, this.$container).val(state.note); 3099 3100 if (cartDiscountList.length === 0) { 3101 $(selectors.cartDiscountWrapper, this.$container) 3102 .html('')
3103 .addClass(classes.hide); 3104 } else { 3105 $(selectors.cartDiscountWrapper, this.$container) 3106 .html(cartDiscountList) 3107 .removeClass(classes.hide); 3108 } 3109 3110 $(selectors.cartSubtotal, this.$container).html( 3111 theme.Currency.formatMoney( 3112 state.total_price, 3113 theme.moneyFormatWithCurrency 3114 ) 3115 ); 3116 }, 3117 3118 _createCartDiscountList: function(cart) { 3119 return $.map( 3120 cart.cart_level_discount_applications, 3121 function(discount) { 3122 var $discount = this.$cartDiscountTemplate.clone(); 3123 $discount.find(selectors.cartDiscountTitle).text(discount.title); 3124 $discount 3125 .find(selectors.cartDiscountAmount) 3126 .html( 3127 theme.Currency.formatMoney( 3128 discount.total_allocated_amount, 3129 theme.moneyFormat 3130 ) 3131 ); 3132 return $discount[0]; 3133 }.bind(this) 3134 ); 3135 }, 3136 3137 _createLineItemList: function(state) { 3138 return $.map( 3139 state.items, 3140 function(item, index) { 3141 var $item = this.$itemTemplate.clone(); 3142 var $itemPriceList = this.$itemPriceListTemplate.clone(); 3143 3144 this._setLineItemAttributes($item, item, index); 3145 this._setLineItemImage($item, item.featured_image); 3146 3147 $(selectors.cartItemTitle, $item) 3148 .text(item.product_title) 3149 .attr('href', item.url); 3150 3151 var productDetailsList = this._createProductDetailsList( 3152 item.product_has_only_default_variant, 3153 item.options_with_values, 3154 item.properties 3155 ); 3156 this._setProductDetailsList($item, productDetailsList); 3157 3158 this._setItemRemove($item, item.title); 3159 3160 $itemPriceList.html( 3161 this._createItemPrice( 3162 item.original_price, 3163 item.final_price, 3164 this.$itemPriceListTemplate 3165 ) 3166 ); 3167 3168 if (item.unit_price_measurement) { 3169 $itemPriceList.append( 3170 this._createUnitPrice( 3171 item.unit_price, 3172 item.unit_price_measurement, 3173 this.$itemPriceListTemplate 3174 ) 3175 ); 3176 } 3177 3178 this._setItemPrice($item, $itemPriceList); 3179 3180 var itemDiscountList = this._createItemDiscountList(item); 3181 this._setItemDiscountList($item, itemDiscountList); 3182 3183 this._setQuantityInputs($item, item, index); 3184 3185 var itemLinePrice = this._createItemPrice( 3186 item.original_line_price, 3187 item.final_line_price, 3188 this.$itemLinePriceTemplate 3189 ); 3190 this._setItemLinePrice($item, itemLinePrice); 3191 3192 return $item[0]; 3193 }.bind(this) 3194 ); 3195 }, 3196 3197 _setLineItemAttributes: function($item, item, index) { 3198 $item 3199 .attr(attributes.cartItemKey, item.key) 3200 .attr(attributes.cartItemUrl, item.url) 3201 .attr(attributes.cartItemTitle, item.title) 3202 .attr(attributes.cartItemIndex, index + 1) 3203 .attr(attributes.cartItemQuantity, item.quantity); 3204 }, 3205 3206 _setLineItemImage: function($item, featuredImage) { 3207 var $image = $(selectors.cartItemImage, $item); 3208 3209 var sizedImageUrl = 3210 featuredImage.url !== null 3211 ? theme.Images.getSizedImageUrl(featuredImage.url, 'x190') 3212 : null; 3213 3214 if (sizedImageUrl) { 3215 $image 3216 .attr('alt', featuredImage.alt) 3217 .attr('src', sizedImageUrl) 3218 .removeClass(classes.hide); 3219 } else { 3220 $image.remove(); 3221 } 3222 }, 3223 3224 _setProductDetailsList: function($item, productDetailsList) { 3225 var $itemDetails = $(selectors.cartItemDetails, $item); 3226 3227 if (productDetailsList.length === 0) {
3228 $itemDetails.addClass(classes.hide).text(''); 3229 } else { 3230 $itemDetails.removeClass(classes.hide).html(productDetailsList); 3231 } 3232 }, 3233 3234 _setItemPrice: function($item, price) { 3235 $(selectors.cartItemPrice, $item).html(price); 3236 }, 3237 3238 _setItemDiscountList: function($item, discountList) { 3239 var $itemDiscountList = $(selectors.cartItemDiscountList, $item); 3240 3241 if (discountList.length === 0) { 3242 $itemDiscountList.html('').addClass(classes.hide); 3243 } else { 3244 $itemDiscountList.html(discountList).removeClass(classes.hide); 3245 } 3246 }, 3247 3248 _setItemRemove: function($item, title) { 3249 $(selectors.cartRemove, $item).attr( 3250 'aria-label', 3251 theme.strings.removeLabel.replace('[product]', title) 3252 ); 3253 }, 3254 3255 _setQuantityInputs: function($item, item, index) { 3256 $(selectors.quantityInputMobile, $item) 3257 .attr('id', 'updates_' + item.key) 3258 .attr(attributes.quantityItem, index + 1) 3259 .val(item.quantity); 3260 3261 $(selectors.quantityInputDesktop, $item) 3262 .attr('id', 'updates_large_' + item.key) 3263 .attr(attributes.quantityItem, index + 1) 3264 .val(item.quantity); 3265 3266 $(selectors.quantityLabelMobile, $item).attr( 3267 'for', 3268 'updates_' + item.key 3269 ); 3270 3271 $(selectors.quantityLabelDesktop, $item).attr( 3272 'for', 3273 'updates_large_' + item.key 3274 ); 3275 }, 3276 3277 setQuantityFormControllers: function() { 3278 if (this.mql.matches) { 3279 $(selectors.quantityInputDesktop).attr('name', 'updates[]'); 3280 $(selectors.quantityInputMobile).removeAttr('name'); 3281 } else { 3282 $(selectors.quantityInputMobile).attr('name', 'updates[]'); 3283 $(selectors.quantityInputDesktop).removeAttr('name'); 3284 } 3285 }, 3286 3287 _setItemLinePrice: function($item, price) { 3288 $(selectors.cartItemLinePrice, $item).html(price); 3289 }, 3290 3291 _createProductDetailsList: function( 3292 product_has_only_default_variant, 3293 options, 3294 properties 3295 ) { 3296 var optionsPropertiesHTML = []; 3297 3298 if (!product_has_only_default_variant) { 3299 optionsPropertiesHTML = optionsPropertiesHTML.concat( 3300 this._getOptionList(options) 3301 ); 3302 } 3303 3304 if (properties !== null && Object.keys(properties).length !== 0) { 3305 optionsPropertiesHTML = optionsPropertiesHTML.concat( 3306 this._getPropertyList(properties) 3307 ); 3308 } 3309 3310 return optionsPropertiesHTML; 3311 }, 3312 3313 _getOptionList: function(options) { 3314 return $.map( 3315 options, 3316 function(option) { 3317 var $optionElement = this.$itemOptionTemplate.clone(); 3318 3319 $optionElement 3320 .text(option.name + ': ' + option.value) 3321 .removeClass(classes.hide); 3322 3323 return $optionElement[0]; 3324 }.bind(this) 3325 ); 3326 }, 3327 3328 _getPropertyList: function(properties) { 3329 var propertiesArray = 3330 properties !== null ? Object.entries(properties) : []; 3331 3332 return $.map( 3333 propertiesArray, 3334 function(property) { 3335 var $propertyElement = this.$itemPropertyTemplate.clone(); 3336 3337 // Line item properties prefixed with an underscore are not to be displayed 3338 if (property[0].charAt(0) === '_') return; 3339 3340 // if the property value has a length of 0 (empty), don't display it 3341 if (property[1].length === 0) return; 3342 3343 $propertyElement 3344 .find(selectors.cartItemPropertyName) 3345 .text(property[0]); 3346 3347 if (property[0].indexOf('/uploads/') === -1) { 3348 $propertyElement 3349 .find(selectors.cartItemPropertyValue) 3350 .text(': ' + property[1]); 3351 } else { 3352 $propertyElement 3353 .find(selectors.cartItemPropertyValue) 3354 .html( 3355 ': <a href="' + 3356 property[1] + 3357 '"> ' + 3358 property[1].split('/').pop() + 3359 '</a>' 3360 ); 3361 } 3362 3363 $propertyElement.removeClass(classes.hide); 3364 3365 return $propertyElement[0]; 3366 }.bind(this) 3367 ); 3368 }, 3369 3370 _createItemPrice: function(original_price, final_price, $priceTemplate) { 3371 if (original_price !== final_price) { 3372 var $discountedPrice = $( 3373 selectors.cartItemDiscountedPriceGroup, 3374 $priceTemplate 3375 ).clone(); 3376 3377 $(selectors.cartItemOriginalPrice, $discountedPrice).html( 3378 theme.Currency.formatMoney(original_price, theme.moneyFormat) 3379 ); 3380 $(selectors.cartItemFinalPrice, $discountedPrice).html( 3381 theme.Currency.formatMoney(final_price, theme.moneyFormat) 3382 );
3383 $discountedPrice.removeClass(classes.hide); 3384 3385 return $discountedPrice[0]; 3386 } else { 3387 var $regularPrice = $( 3388 selectors.cartItemRegularPriceGroup, 3389 $priceTemplate 3390 ).clone(); 3391 3392 $(selectors.cartItemRegularPrice, $regularPrice).html( 3393 theme.Currency.formatMoney(original_price, theme.moneyFormat) 3394 ); 3395 3396 $regularPrice.removeClass(classes.hide); 3397 3398 return $regularPrice[0]; 3399 } 3400 }, 3401 3402 _createUnitPrice: function( 3403 unitPrice, 3404 unitPriceMeasurement, 3405 $itemPriceGroup 3406 ) { 3407 var $unitPriceGroup = $( 3408 selectors.unitPriceGroup, 3409 $itemPriceGroup 3410 ).clone(); 3411 3412 var unitPriceBaseUnit = 3413 (unitPriceMeasurement.reference_value !== 1 3414 ? unitPriceMeasurement.reference_value 3415 : '') + unitPriceMeasurement.reference_unit; 3416 3417 $(selectors.unitPriceBaseUnit, $unitPriceGroup).text(unitPriceBaseUnit); 3418 $(selectors.unitPrice, $unitPriceGroup).html( 3419 theme.Currency.formatMoney(unitPrice, theme.moneyFormat) 3420 ); 3421 3422 $unitPriceGroup.removeClass(classes.hide); 3423 3424 return $unitPriceGroup[0]; 3425 }, 3426 3427 _createItemDiscountList: function(item) { 3428 return $.map( 3429 item.line_level_discount_allocations, 3430 function(discount) { 3431 var $discount = this.$itemDiscountTemplate.clone(); 3432 $discount 3433 .find(selectors.cartItemDiscountTitle) 3434 .text(discount.discount_application.title); 3435 $discount 3436 .find(selectors.cartItemDiscountAmount) 3437 .html( 3438 theme.Currency.formatMoney(discount.amount, theme.moneyFormat) 3439 ); 3440 return $discount[0]; 3441 }.bind(this) 3442 ); 3443 }, 3444 3445 _showQuantityErrorMessages: function(itemElement) { 3446 $(selectors.cartQuantityErrorMessage, itemElement).text( 3447 theme.strings.quantityMinimumMessage 3448 ); 3449 3450 $(selectors.cartQuantityErrorMessageWrapper, itemElement).removeClass( 3451 classes.hide 3452 ); 3453 3454 $(selectors.inputQty, itemElement)
3455 .addClass(classes.inputError) 3456 .focus(); 3457 }, 3458 3459 _hideQuantityErrorMessage: function() { 3460 var $errorMessages = $( 3461 selectors.cartQuantityErrorMessageWrapper 3462 ).addClass(classes.hide); 3463 3464 $(selectors.cartQuantityErrorMessage, $errorMessages).text(''); 3465 3466 $(selectors.inputQty, this.$container).removeClass(classes.inputError); 3467 }, 3468 3469 _handleThumbnailClick: function(evt) { 3470 var url = $(evt.target) 3471 .closest(selectors.cartItem) 3472 .data('cart-item-url'); 3473 3474 window.location.href = url; 3475 }, 3476 3477 _onNoteChange: function(evt) { 3478 var note = evt.currentTarget.value; 3479 this._hideCartError(); 3480 this._hideQuantityErrorMessage(); 3481 3482 var params = { 3483 url: '/cart/update.js', 3484 data: { note: note }, 3485 dataType: 'json' 3486 }; 3487 3488 $.post(params).fail( 3489 function() { 3490 this._showCartError(evt.currentTarget); 3491 }.bind(this) 3492 ); 3493 }, 3494 3495 _showCartError: function(elementToFocus) { 3496 $(selectors.cartErrorMessage).text(theme.strings.cartError); 3497 3498 $(selectors.cartErrorMessageWrapper).removeClass(classes.hide); 3499 3500 elementToFocus.focus(); 3501 }, 3502 3503 _hideCartError: function() {
3504 $(selectors.cartErrorMessageWrapper).addClass(classes.hide); 3505 $(selectors.cartErrorMessage).text(''); 3506 }, 3507 3508 _onRemoveItem: function(evt) { 3509 evt.preventDefault(); 3510 var $remove = $(evt.target); 3511 var $lineItem = $remove.closest(selectors.cartItem); 3512 var index = $lineItem.attr(attributes.cartItemIndex); 3513 this._hideCartError(); 3514 3515 var params = { 3516 url: '/cart/change.js', 3517 data: { quantity: 0, line: index }, 3518 dataType: 'json' 3519 }; 3520 3521 $.post(params) 3522 .done( 3523 function(state) { 3524 if (state.item_count === 0) { 3525 this._emptyCart(); 3526 } else { 3527 this._createCart(state); 3528 this._showRemoveMessage($lineItem.clone()); 3529 } 3530 3531 this._setCartCountBubble(state.item_count); 3532 }.bind(this) 3533 ) 3534 .fail( 3535 function() { 3536 this._showCartError(null); 3537 }.bind(this) 3538 ); 3539 }, 3540 3541 _showRemoveMessage: function(lineItem) { 3542 var index = lineItem.data('cart-item-index'); 3543 var removeMessage = this._getRemoveMessage(lineItem); 3544 var $lineItemAtIndex; 3545 3546 if (index - 1 === 0) { 3547 $lineItemAtIndex = $('[data-cart-item-index="1"]', this.$container); 3548 $(removeMessage).insertBefore($lineItemAtIndex); 3549 } else { 3550 $lineItemAtIndex = $( 3551 '[data-cart-item-index="' + (index - 1) + '"]', 3552 this.$container 3553 ); 3554 removeMessage.insertAfter($lineItemAtIndex); 3555 } 3556 removeMessage.focus(); 3557 }, 3558 3559 _getRemoveMessage: function(lineItem) { 3560 var formattedMessage = this._formatRemoveMessage(lineItem); 3561 3562 var $tableCell = $(selectors.cartTableCell, lineItem).clone(); 3563 $tableCell 3564 .removeClass()
3565 .addClass(classes.cartRemovedProduct) 3566 .attr('colspan', '4') 3567 .html(formattedMessage); 3568 3569 lineItem 3570 .attr('role', 'alert') 3571 .html($tableCell) 3572 .attr('tabindex', '-1'); 3573 3574 return lineItem; 3575 }, 3576 3577 _formatRemoveMessage: function(lineItem) { 3578 var quantity = lineItem.data('cart-item-quantity'); 3579 var url = lineItem.attr(attributes.cartItemUrl); 3580 var title = lineItem.attr(attributes.cartItemTitle); 3581 3582 return theme.strings.removedItemMessage 3583 .replace('[quantity]', quantity) 3584 .replace( 3585 '[link]', 3586 '<a ' + 3587 'href="' + 3588 url + 3589 '" class="text-link text-link--accent">' + 3590 title + 3591 '</a>' 3592 ); 3593 }, 3594 3595 _setCartCountBubble: function(quantity) { 3596 this.$cartCountBubble = 3597 this.$cartCountBubble || $(selectors.cartCountBubble); 3598 this.$cartCount = this.$cartCount || $(selectors.cartCount); 3599 3600 if (quantity > 0) { 3601 this.$cartCountBubble.removeClass(classes.hide); 3602 this.$cartCount.html(quantity); 3603 } else {
3604 this.$cartCountBubble.addClass(classes.hide); 3605 this.$cartCount.html(''); 3606 } 3607 }, 3608 3609 _emptyCart: function() { 3610 this.$emptyPageContent = 3611 this.$emptyPageContent || 3612 $(selectors.emptyPageContent, this.$container); 3613 this.$cartWrapper = 3614 this.$cartWrapper || $(selectors.cartWrapper, this.$container); 3615 3616 this.$emptyPageContent.removeClass(classes.hide); 3617 this.$cartWrapper.addClass(classes.hide); 3618 }, 3619 3620 cookiesEnabled: function() { 3621 var cookieEnabled = navigator.cookieEnabled; 3622 3623 if (!cookieEnabled) { 3624 document.cookie = 'testcookie'; 3625 cookieEnabled = document.cookie.indexOf('testcookie') !== -1; 3626 } 3627 return cookieEnabled; 3628 } 3629 }); 3630 3631 return Cart; 3632})(); 3633 3634window.theme = window.theme || {}; 3635 3636theme.Filters = (function() { 3637 var settings = { 3638 // Breakpoints from src/stylesheets/global/variables.scss.liquid 3639 mediaQueryMediumUp: 'screen and (min-width: 750px)' 3640 }; 3641 3642 var selectors = { 3643 mainContent: '#MainContent', 3644 filterSelection: '#FilterTags', 3645 sortSelection: '#SortBy' 3646 }; 3647 3648 var data = { 3649 sortBy: 'data-default-sortby' 3650 }; 3651 3652 function Filters(container) { 3653 var $container = (this.$container = $(container)); 3654 3655 this.$filterSelect = $(selectors.filterSelection, $container); 3656 this.$sortSelect = $(selectors.sortSelection, $container); 3657 this.$selects = $(selectors.filterSelection, $container).add( 3658 $(selectors.sortSelection, $container) 3659 ); 3660 3661 this.defaultSort = this._getDefaultSortValue(); 3662 this.$selects.removeClass('hidden'); 3663 3664 this.$filterSelect.on('change', this._onFilterChange.bind(this)); 3665 this.$sortSelect.on('change', this._onSortChange.bind(this)); 3666 this._initBreakpoints(); 3667 this._initParams(); 3668 } 3669 3670 Filters.prototype = _.assignIn({}, Filters.prototype, { 3671 _initBreakpoints: function() { 3672 var self = this; 3673 3674 enquire.register(settings.mediaQueryMediumUp, { 3675 match: function() { 3676 self._resizeSelect(self.$selects); 3677 } 3678 }); 3679 }, 3680 3681 _initParams: function() { 3682 self.queryParams = {}; 3683 if (location.search.length) { 3684 var aKeyValue; 3685 var aCouples = location.search.substr(1).split('&'); 3686 for (var i = 0; i < aCouples.length; i++) { 3687 aKeyValue = aCouples[i].split('='); 3688 if (aKeyValue.length > 1) { 3689 self.queryParams[ 3690 decodeURIComponent(aKeyValue[0]) 3691 ] = decodeURIComponent(aKeyValue[1]); 3692 } 3693 } 3694 } 3695 }, 3696 3697 _onSortChange: function() { 3698 self.queryParams.sort_by = this._getSortValue(); 3699 3700 if (self.queryParams.page) { 3701 delete self.queryParams.page; 3702 } 3703 window.location.search = decodeURIComponent($.param(self.queryParams)); 3704 }, 3705 3706 _onFilterChange: function() { 3707 document.location.href = this._getFilterValue(); 3708 }, 3709 3710 _getFilterValue: function() { 3711 return this.$filterSelect.val(); 3712 }, 3713 3714 _getSortValue: function() { 3715 return this.$sortSelect.val() || this.defaultSort; 3716 }, 3717 3718 _getDefaultSortValue: function() { 3719 return this.$sortSelect.attr(data.sortBy); 3720 }, 3721 3722 _resizeSelect: function($selection) { 3723 $selection.each(function() { 3724 var $this = $(this); 3725 var arrowWidth = 10; 3726 // create test element 3727 var text = $this.find('option:selected').text(); 3728 var $test = $('<span>').html(text); 3729 3730 // add to body, get width, and get out 3731 $test.appendTo('body'); 3732 var width = $test.width(); 3733 $test.remove(); 3734 3735 // set select width 3736 $this.width(width + arrowWidth); 3737 }); 3738 }, 3739 3740 onUnload: function() { 3741 this.$filterSelect.off('change', this._onFilterChange); 3742 this.$sortSelect.off('change', this._onSortChange); 3743 } 3744 }); 3745 3746 return Filters; 3747})(); 3748 3749window.theme = window.theme || {}; 3750 3751theme.HeaderSection = (function() { 3752 function Header() { 3753 theme.Header.init(); 3754 theme.MobileNav.init(); 3755 theme.Search.init(); 3756 } 3757 3758 Header.prototype = _.assignIn({}, Header.prototype, { 3759 onUnload: function() { 3760 theme.Header.unload(); 3761 } 3762 }); 3763 3764 return Header; 3765})(); 3766 3767theme.Maps = (function() { 3768 var config = { 3769 zoom: 14 3770 }; 3771 var apiStatus = null; 3772 var mapsToLoad = []; 3773 3774 var errors = { 3775 addressNoResults: theme.strings.addressNoResults, 3776 addressQueryLimit: theme.strings.addressQueryLimit, 3777 addressError: theme.strings.addressError, 3778 authError: theme.strings.authError 3779 }; 3780 3781 var selectors = { 3782 section: '[data-section-type="map"]', 3783 map: '[data-map]', 3784 mapOverlay: '[data-map-overlay]' 3785 }; 3786 3787 var classes = { 3788 mapError: 'map-section--load-error', 3789 errorMsg: 'map-section__error errors text-center' 3790 }; 3791 3792 // Global function called by Google on auth errors. 3793 // Show an auto error message on all map instances. 3794 // eslint-disable-next-line camelcase, no-unused-vars 3795 window.gm_authFailure = function() { 3796 if (!Shopify.designMode) { 3797 return; 3798 } 3799
3800 $(selectors.section).addClass(classes.mapError); 3801 $(selectors.map).remove(); 3802 $(selectors.mapOverlay).after( 3803 '<div class="' + 3804 classes.errorMsg + 3805 '">' + 3806 theme.strings.authError + 3807 '</div>' 3808 ); 3809 }; 3810 3811 function Map(container) { 3812 this.$container = $(container); 3813 this.$map = this.$container.find(selectors.map); 3814 this.key = this.$map.data('api-key'); 3815 3816 if (typeof this.key === 'undefined') { 3817 return; 3818 } 3819 3820 if (apiStatus === 'loaded') { 3821 this.createMap(); 3822 } else { 3823 mapsToLoad.push(this); 3824 3825 if (apiStatus !== 'loading') { 3826 apiStatus = 'loading'; 3827 if (typeof window.google === 'undefined') { 3828 $.getScript( 3829 'https://maps.googleapis.com/maps/api/js?key=' + this.key 3830 ).then(function() { 3831 apiStatus = 'loaded'; 3832 initAllMaps(); 3833 }); 3834 } 3835 } 3836 } 3837 } 3838 3839 function initAllMaps() { 3840 // API has loaded, load all Map instances in queue 3841 $.each(mapsToLoad, function(index, instance) { 3842 instance.createMap(); 3843 }); 3844 } 3845 3846 function geolocate($map) { 3847 var deferred = $.Deferred(); 3848 var geocoder = new google.maps.Geocoder(); 3849 var address = $map.data('address-setting'); 3850 3851 geocoder.geocode({ address: address }, function(results, status) { 3852 if (status !== google.maps.GeocoderStatus.OK) { 3853 deferred.reject(status); 3854 } 3855 3856 deferred.resolve(results); 3857 }); 3858 3859 return deferred; 3860 } 3861 3862 Map.prototype = _.assignIn({}, Map.prototype, { 3863 createMap: function() { 3864 var $map = this.$map; 3865 3866 return geolocate($map) 3867 .then( 3868 function(results) { 3869 var mapOptions = { 3870 zoom: config.zoom, 3871 center: results[0].geometry.location, 3872 draggable: false, 3873 clickableIcons: false, 3874 scrollwheel: false, 3875 disableDoubleClickZoom: true, 3876 disableDefaultUI: true 3877 }; 3878 3879 var map = (this.map = new google.maps.Map($map[0], mapOptions)); 3880 var center = (this.center = map.getCenter()); 3881 3882 //eslint-disable-next-line no-unused-vars 3883 var marker = new google.maps.Marker({ 3884 map: map, 3885 position: map.getCenter() 3886 }); 3887 3888 google.maps.event.addDomListener( 3889 window, 3890 'resize', 3891 $.debounce(250, function() { 3892 google.maps.event.trigger(map, 'resize'); 3893 map.setCenter(center); 3894 $map.removeAttr('style'); 3895 }) 3896 ); 3897 }.bind(this) 3898 ) 3899 .fail(function() { 3900 var errorMessage; 3901 3902 switch (status) { 3903 case 'ZERO_RESULTS': 3904 errorMessage = errors.addressNoResults; 3905 break; 3906 case 'OVER_QUERY_LIMIT': 3907 errorMessage = errors.addressQueryLimit; 3908 break; 3909 case 'REQUEST_DENIED': 3910 errorMessage = errors.authError; 3911 break; 3912 default: 3913 errorMessage = errors.addressError; 3914 break; 3915 } 3916 3917 // Show errors only to merchant in the editor. 3918 if (Shopify.designMode) { 3919 $map 3920 .parent()
3921 .addClass(classes.mapError) 3922 .html( 3923 '<div class="' + 3924 classes.errorMsg + 3925 '">' + 3926 errorMessage + 3927 '</div>' 3928 ); 3929 } 3930 }); 3931 }, 3932 3933 onUnload: function() { 3934 if (this.$map.length === 0) { 3935 return; 3936 } 3937 google.maps.event.clearListeners(this.map, 'resize'); 3938 } 3939 }); 3940 3941 return Map; 3942})(); 3943 3944/* eslint-disable no-new */ 3945theme.Product = (function() { 3946 function Product(container) { 3947 var $container = (this.$container = $(container)); 3948 var sectionId = $container.attr('data-section-id'); 3949 this.ajaxEnabled = $container.data('ajax-enabled'); 3950 3951 this.settings = { 3952 // Breakpoints from src/stylesheets/global/variables.scss.liquid 3953 mediaQueryMediumUp: 'screen and (min-width: 750px)', 3954 mediaQuerySmall: 'screen and (max-width: 749px)', 3955 bpSmall: false, 3956 enableHistoryState: $container.data('enable-history-state') || false, 3957 namespace: '.slideshow-' + sectionId, 3958 sectionId: sectionId, 3959 sliderActive: false, 3960 zoomEnabled: false 3961 }; 3962 3963 this.selectors = { 3964 addToCart: '[data-add-to-cart]', 3965 addToCartText: '[data-add-to-cart-text]', 3966 cartCount: '[data-cart-count]', 3967 cartCountBubble: '[data-cart-count-bubble]', 3968 cartPopup: '[data-cart-popup]', 3969 cartPopupCartQuantity: '[data-cart-popup-cart-quantity]', 3970 cartPopupClose: '[data-cart-popup-close]', 3971 cartPopupDismiss: '[data-cart-popup-dismiss]', 3972 cartPopupImage: '[data-cart-popup-image]', 3973 cartPopupImageWrapper: '[data-cart-popup-image-wrapper]', 3974 cartPopupImagePlaceholder: '[data-cart-popup-image-placeholder]', 3975 cartPopupPlaceholderSize: '[data-placeholder-size]', 3976 cartPopupProductDetails: '[data-cart-popup-product-details]', 3977 cartPopupQuantity: '[data-cart-popup-quantity]', 3978 cartPopupQuantityLabel: '[data-cart-popup-quantity-label]', 3979 cartPopupTitle: '[data-cart-popup-title]', 3980 cartPopupWrapper: '[data-cart-popup-wrapper]', 3981 loader: '[data-loader]', 3982 loaderStatus: '[data-loader-status]', 3983 quantity: '[data-quantity-input]', 3984 SKU: '.variant-sku', 3985 productStatus: '[data-product-status]', 3986 originalSelectorId: '#ProductSelect-' + sectionId, 3987 productForm: '[data-product-form]', 3988 errorMessage: '[data-error-message]', 3989 errorMessageWrapper: '[data-error-message-wrapper]', 3990 productImageWraps: '.product-single__photo', 3991 productThumbImages: '.product-single__thumbnail--' + sectionId, 3992 productThumbs: '.product-single__thumbnails-' + sectionId, 3993 productThumbListItem: '.product-single__thumbnails-item', 3994 productFeaturedImage: '.product-featured-img', 3995 productThumbsWrapper: '.thumbnails-wrapper', 3996 saleLabel: '.product-price__sale-label-' + sectionId, 3997 singleOptionSelector: '.single-option-selector-' + sectionId, 3998 shopifyPaymentButton: '.shopify-payment-button', 3999 priceContainer: '[data-price]', 4000 regularPrice: '[data-regular-price]', 4001 salePrice: '[data-sale-price]', 4002 unitPrice: '[data-unit-price]', 4003 unitPriceBaseUnit: '[data-unit-price-base-unit]' 4004 }; 4005 4006 this.classes = { 4007 cartPopupWrapperHidden: 'cart-popup-wrapper--hidden', 4008 hidden: 'hide', 4009 inputError: 'input--error', 4010 productOnSale: 'price--on-sale', 4011 productUnitAvailable: 'price--unit-available', 4012 productUnavailable: 'price--unavailable', 4013 cartImage: 'cart-popup-item__image', 4014 productFormErrorMessageWrapperHidden: 4015 'product-form__error-message-wrapper--hidden', 4016 activeClass: 'active-thumb' 4017 }; 4018 4019 this.$quantityInput = $(this.selectors.quantity, $container); 4020 this.$errorMessageWrapper = $( 4021 this.selectors.errorMessageWrapper, 4022 $container 4023 ); 4024 this.$addToCart = $(this.selectors.addToCart, $container); 4025 this.$addToCartText = $(this.selectors.addToCartText, this.$addToCart); 4026 this.$shopifyPaymentButton = $( 4027 this.selectors.shopifyPaymentButton, 4028 $container 4029 ); 4030 4031 this.$loader = $(this.selectors.loader, this.$addToCart); 4032 this.$loaderStatus = $(this.selectors.loaderStatus, $container); 4033 4034 // Stop parsing if we don't have the product json script tag when loading 4035 // section in the Theme Editor 4036 if (!$('#ProductJson-' + sectionId).html()) { 4037 return; 4038 } 4039 4040 this.productSingleObject = JSON.parse( 4041 document.getElementById('ProductJson-' + sectionId).innerHTML 4042 ); 4043 4044 this.settings.zoomEnabled = $(this.selectors.productImageWraps).hasClass( 4045 'js-zoom-enabled' 4046 ); 4047 4048 this._initBreakpoints(); 4049 this._stringOverrides(); 4050 this._initVariants(); 4051 this._initImageSwitch(); 4052 this._initAddToCart();
4053 this._setActiveThumbnail(); 4054 } 4055 4056 Product.prototype = _.assignIn({}, Product.prototype, { 4057 _stringOverrides: function() { 4058 theme.productStrings = theme.productStrings || {}; 4059 $.extend(theme.strings, theme.productStrings); 4060 }, 4061 4062 _initBreakpoints: function() { 4063 var self = this; 4064 4065 enquire.register(this.settings.mediaQuerySmall, { 4066 match: function() { 4067 // initialize thumbnail slider on mobile if more than three thumbnails 4068 if ($(self.selectors.productThumbImages).length > 3) { 4069 self._initThumbnailSlider(); 4070 } 4071 4072 // destroy image zooming if enabled 4073 if (self.settings.zoomEnabled) { 4074 $(self.selectors.productImageWraps).each(function() { 4075 _destroyZoom(this); 4076 }); 4077 } 4078 4079 self.settings.bpSmall = true; 4080 }, 4081 unmatch: function() { 4082 if (self.settings.sliderActive) { 4083 self._destroyThumbnailSlider(); 4084 } 4085 4086 self.settings.bpSmall = false; 4087 } 4088 }); 4089 4090 enquire.register(this.settings.mediaQueryMediumUp, { 4091 match: function() { 4092 if (self.settings.zoomEnabled) { 4093 $(self.selectors.productImageWraps).each(function() { 4094 _enableZoom(this); 4095 }); 4096 } 4097 } 4098 }); 4099 }, 4100 4101 _initVariants: function() { 4102 var options = { 4103 $container: this.$container, 4104 enableHistoryState: 4105 this.$container.data('enable-history-state') || false, 4106 singleOptionSelector: this.selectors.singleOptionSelector, 4107 originalSelectorId: this.selectors.originalSelectorId, 4108 product: this.productSingleObject 4109 }; 4110 4111 this.variants = new slate.Variants(options); 4112 4113 this.$container.on( 4114 'variantChange' + this.settings.namespace, 4115 this._updateAvailability.bind(this) 4116 ); 4117 this.$container.on( 4118 'variantImageChange' + this.settings.namespace, 4119 this._updateImages.bind(this) 4120 ); 4121 this.$container.on( 4122 'variantPriceChange' + this.settings.namespace, 4123 this._updatePrice.bind(this) 4124 ); 4125 this.$container.on( 4126 'variantSKUChange' + this.settings.namespace, 4127 this._updateSKU.bind(this) 4128 ); 4129 }, 4130 4131 _initImageSwitch: function() { 4132 if (!$(this.selectors.productThumbImages).length) { 4133 return; 4134 } 4135 4136 var self = this; 4137 4138 $(this.selectors.productThumbImages) 4139 .on('click', function(evt) { 4140 evt.preventDefault(); 4141 var $el = $(this); 4142 4143 var imageId = $el.data('thumbnail-id'); 4144 4145 self._switchImage(imageId); 4146 self._setActiveThumbnail(imageId); 4147 }) 4148 .on('keyup', self._handleImageFocus.bind(self)); 4149 }, 4150 4151 _initAddToCart: function() { 4152 $(this.selectors.productForm, this.$container).on( 4153 'submit', 4154 function(evt) { 4155 if (this.$addToCart.is('[aria-disabled]')) { 4156 evt.preventDefault(); 4157 return; 4158 } 4159 4160 if (!this.ajaxEnabled) return; 4161 4162 evt.preventDefault(); 4163 4164 this.$previouslyFocusedElement = $(':focus'); 4165 4166 var isInvalidQuantity = this.$quantityInput.val() <= 0; 4167 4168 if (isInvalidQuantity) { 4169 this._showErrorMessage(theme.strings.quantityMinimumMessage); 4170 return; 4171 } 4172 4173 if (!isInvalidQuantity && this.ajaxEnabled) { 4174 // disable the addToCart and dynamic checkout button while 4175 // request/cart popup is loading and handle loading state 4176 this._handleButtonLoadingState(true); 4177 var $data = $(this.selectors.productForm, this.$container); 4178 this._addItemToCart($data); 4179 return; 4180 } 4181 }.bind(this) 4182 ); 4183 }, 4184 4185 _addItemToCart: function(data) { 4186 var params = { 4187 url: '/cart/add.js', 4188 data: $(data).serialize(), 4189 dataType: 'json' 4190 }; 4191 4192 $.post(params) 4193 .done( 4194 function(item) { 4195 this._hideErrorMessage(); 4196 this._setupCartPopup(item); 4197 }.bind(this) 4198 ) 4199 .fail( 4200 function(response) { 4201 this.$previouslyFocusedElement.focus(); 4202 var errorMessage = response.responseJSON 4203 ? response.responseJSON.description 4204 : theme.strings.cartError; 4205 this._showErrorMessage(errorMessage); 4206 this._handleButtonLoadingState(false); 4207 }.bind(this) 4208 ); 4209 }, 4210 4211 _handleButtonLoadingState: function(isLoading) { 4212 if (isLoading) { 4213 this.$addToCart.attr('aria-disabled', true); 4214 this.$addToCartText.addClass(this.classes.hidden); 4215 this.$loader.removeClass(this.classes.hidden); 4216 this.$shopifyPaymentButton.attr('disabled', true); 4217 this.$loaderStatus.attr('aria-hidden', false); 4218 } else { 4219 this.$addToCart.removeAttr('aria-disabled'); 4220 this.$addToCartText.removeClass(this.classes.hidden); 4221 this.$loader.addClass(this.classes.hidden); 4222 this.$shopifyPaymentButton.removeAttr('disabled'); 4223 this.$loaderStatus.attr('aria-hidden', true); 4224 } 4225 }, 4226 4227 _showErrorMessage: function(errorMessage) { 4228 $(this.selectors.errorMessage, this.$container).html(errorMessage); 4229 4230 if (this.$quantityInput.length !== 0) { 4231 this.$quantityInput.addClass(this.classes.inputError); 4232 } 4233 4234 this.$errorMessageWrapper 4235 .removeClass(this.classes.productFormErrorMessageWrapperHidden) 4236 .attr('aria-hidden', true) 4237 .removeAttr('aria-hidden'); 4238 }, 4239 4240 _hideErrorMessage: function() { 4241 this.$errorMessageWrapper.addClass( 4242 this.classes.productFormErrorMessageWrapperHidden 4243 ); 4244 4245 if (this.$quantityInput.length !== 0) { 4246 this.$quantityInput.removeClass(this.classes.inputError); 4247 } 4248 }, 4249 4250 _setupCartPopup: function(item) { 4251 this.$cartPopup = this.$cartPopup || $(this.selectors.cartPopup); 4252 this.$cartPopupWrapper = 4253 this.$cartPopupWrapper || $(this.selectors.cartPopupWrapper); 4254 this.$cartPopupTitle = 4255 this.$cartPopupTitle || $(this.selectors.cartPopupTitle); 4256 this.$cartPopupQuantity = 4257 this.$cartPopupQuantity || $(this.selectors.cartPopupQuantity); 4258 this.$cartPopupQuantityLabel = 4259 this.$cartPopupQuantityLabel || 4260 $(this.selectors.cartPopupQuantityLabel); 4261 this.$cartPopupClose = 4262 this.$cartPopupClose || $(this.selectors.cartPopupClose); 4263 this.$cartPopupDismiss = 4264 this.$cartPopupDismiss || $(this.selectors.cartPopupDismiss); 4265 this.$cartPopupImagePlaceholder = 4266 this.$cartPopupImagePlaceholder || 4267 $(this.selectors.cartPopupImagePlaceholder); 4268 4269 this._setupCartPopupEventListeners(); 4270 4271 this._updateCartPopupContent(item); 4272 }, 4273 4274 _updateCartPopupContent: function(item) { 4275 var quantity = this.$quantityInput.length ? this.$quantityInput.val() : 1; 4276 4277 this.$cartPopupTitle.text(item.product_title); 4278 this.$cartPopupQuantity.text(quantity); 4279 this.$cartPopupQuantityLabel.text( 4280 theme.strings.quantityLabel.replace('[count]', quantity) 4281 ); 4282 4283 this._setCartPopupPlaceholder( 4284 item.featured_image.url,
4285 item.featured_image.aspect_ratio 4286 ); 4287 this._setCartPopupImage(item.featured_image.url, item.featured_image.alt); 4288 this._setCartPopupProductDetails( 4289 item.product_has_only_default_variant, 4290 item.options_with_values, 4291 item.properties 4292 ); 4293 4294 $.getJSON('/cart.js').then( 4295 function(cart) { 4296 this._setCartQuantity(cart.item_count); 4297 this._setCartCountBubble(cart.item_count); 4298 this._showCartPopup(); 4299 }.bind(this) 4300 ); 4301 }, 4302 4303 _setupCartPopupEventListeners: function() { 4304 this.$cartPopupWrapper.on( 4305 'keyup', 4306 function(event) { 4307 if (event.keyCode === slate.utils.keyboardKeys.ESCAPE) { 4308 this._hideCartPopup(event); 4309 } 4310 }.bind(this) 4311 ); 4312 4313 this.$cartPopupClose.on('click', this._hideCartPopup.bind(this)); 4314 this.$cartPopupDismiss.on('click', this._hideCartPopup.bind(this)); 4315 $('body').on('click', this._onBodyClick.bind(this)); 4316 }, 4317 4318 _setCartPopupPlaceholder: function(imageUrl, imageAspectRatio) { 4319 this.$cartPopupImageWrapper = 4320 this.$cartPopupImageWrapper || $(this.selectors.cartPopupImageWrapper); 4321 4322 if (imageUrl === null) { 4323 this.$cartPopupImageWrapper.addClass(this.classes.hidden); 4324 return; 4325 } 4326 4327 var $placeholder = $(this.selectors.cartPopupPlaceholderSize); 4328 var maxWidth = 95 * imageAspectRatio; 4329 var heightRatio = 100 / imageAspectRatio; 4330 4331 this.$cartPopupImagePlaceholder.css('max-width', maxWidth); 4332 4333 $placeholder.css('padding-top', heightRatio + '%'); 4334 }, 4335 4336 _setCartPopupImage: function(imageUrl, imageAlt) { 4337 if (imageUrl === null) return; 4338 4339 this.$cartPopupImageWrapper.removeClass(this.classes.hidden); 4340 var sizedImageUrl = theme.Images.getSizedImageUrl(imageUrl, '200x'); 4341 var image = document.createElement('img'); 4342 image.src = sizedImageUrl; 4343 image.alt = imageAlt; 4344 image.classList.add(this.classes.cartImage); 4345 image.dataset.cartPopupImage = ''; 4346 4347 image.onload = function() { 4348 this.$cartPopupImagePlaceholder.addClass(this.classes.hidden); 4349 this.$cartPopupImageWrapper.append(image); 4350 }.bind(this); 4351 }, 4352 4353 _setCartPopupProductDetails: function( 4354 product_has_only_default_variant, 4355 options, 4356 properties 4357 ) { 4358 this.$cartPopupProductDetails = 4359 this.$cartPopupProductDetails || 4360 $(this.selectors.cartPopupProductDetails); 4361 var variantPropertiesHTML = ''; 4362 4363 if (!product_has_only_default_variant) { 4364 variantPropertiesHTML = 4365 variantPropertiesHTML + this._getVariantOptionList(options); 4366 } 4367 4368 if (properties !== null && Object.keys(properties).length !== 0) { 4369 variantPropertiesHTML = 4370 variantPropertiesHTML + this._getPropertyList(properties); 4371 } 4372 4373 if (variantPropertiesHTML.length === 0) { 4374 this.$cartPopupProductDetails.html(''); 4375 this.$cartPopupProductDetails.attr('hidden', ''); 4376 } else { 4377 this.$cartPopupProductDetails.html(variantPropertiesHTML); 4378 this.$cartPopupProductDetails.removeAttr('hidden'); 4379 } 4380 }, 4381 4382 _getVariantOptionList: function(variantOptions) { 4383 var variantOptionListHTML = ''; 4384 4385 variantOptions.forEach(function(variantOption) { 4386 variantOptionListHTML = 4387 variantOptionListHTML + 4388 '<li class="product-details__item product-details__item--variant-option">' + 4389 variantOption.name + 4390 ': ' + 4391 variantOption.value + 4392 '</li>'; 4393 }); 4394 4395 return variantOptionListHTML; 4396 }, 4397 4398 _getPropertyList: function(properties) { 4399 var propertyListHTML = ''; 4400 var propertiesArray = Object.entries(properties); 4401 4402 propertiesArray.forEach(function(property) { 4403 // Line item properties prefixed with an underscore are not to be displayed 4404 if (property[0].charAt(0) === '_') return; 4405 4406 // if the property value has a length of 0 (empty), don't display it 4407 if (property[1].length === 0) return; 4408 4409 propertyListHTML = 4410 propertyListHTML + 4411 '<li class="product-details__item product-details__item--property">' + 4412 '<span class="product-details__property-label">' + 4413 property[0] + 4414 ': </span>' + 4415 property[1]; 4416 ': ' + '</li>'; 4417 }); 4418 4419 return propertyListHTML; 4420 }, 4421 4422 _setCartQuantity: function(quantity) { 4423 this.$cartPopupCartQuantity = 4424 this.$cartPopupCartQuantity || $(this.selectors.cartPopupCartQuantity); 4425 var ariaLabel; 4426 4427 if (quantity === 1) { 4428 ariaLabel = theme.strings.oneCartCount; 4429 } else if (quantity > 1) { 4430 ariaLabel = theme.strings.otherCartCount.replace('[count]', quantity); 4431 } 4432 4433 this.$cartPopupCartQuantity.text(quantity).attr('aria-label', ariaLabel); 4434 }, 4435 4436 _setCartCountBubble: function(quantity) { 4437 this.$cartCountBubble = 4438 this.$cartCountBubble || $(this.selectors.cartCountBubble); 4439 this.$cartCount = this.$cartCount || $(this.selectors.cartCount); 4440 4441 this.$cartCountBubble.removeClass(this.classes.hidden); 4442 this.$cartCount.text(quantity); 4443 }, 4444 4445 _showCartPopup: function() { 4446 this.$cartPopupWrapper 4447 .prepareTransition() 4448 .removeClass(this.classes.cartPopupWrapperHidden); 4449 this._handleButtonLoadingState(false); 4450 4451 slate.a11y.trapFocus({ 4452 $container: this.$cartPopupWrapper, 4453 $elementToFocus: this.$cartPopup, 4454 namespace: 'cartPopupFocus' 4455 }); 4456 }, 4457 4458 _hideCartPopup: function(event) { 4459 var setFocus = event.detail === 0 ? true : false;
4460 this.$cartPopupWrapper 4461 .prepareTransition() 4462 .addClass(this.classes.cartPopupWrapperHidden); 4463 4464 $(this.selectors.cartPopupImage).remove(); 4465 this.$cartPopupImagePlaceholder.removeClass(this.classes.hidden); 4466 4467 slate.a11y.removeTrapFocus({ 4468 $container: this.$cartPopupWrapper, 4469 namespace: 'cartPopupFocus' 4470 }); 4471 4472 if (setFocus) this.$previouslyFocusedElement[0].focus(); 4473 4474 this.$cartPopupWrapper.off('keyup'); 4475 this.$cartPopupClose.off('click'); 4476 this.$cartPopupDismiss.off('click'); 4477 $('body').off('click'); 4478 }, 4479 4480 _onBodyClick: function(event) { 4481 var $target = $(event.target); 4482 4483 if ( 4484 $target[0] !== this.$cartPopupWrapper[0] && 4485 !$target.parents(this.selectors.cartPopup).length 4486 ) { 4487 this._hideCartPopup(event); 4488 } 4489 }, 4490 4491 _setActiveThumbnail: function(imageId) { 4492 // If there is no element passed, find it by the current product image 4493 if (typeof imageId === 'undefined') { 4494 imageId = $( 4495 this.selectors.productImageWraps + ':not(.hide)', 4496 this.$container 4497 ).data('image-id'); 4498 } 4499 4500 var $thumbnailWrappers = $( 4501 this.selectors.productThumbListItem + ':not(.slick-cloned)', 4502 this.$container 4503 ); 4504 4505 var $activeThumbnail = $thumbnailWrappers.find( 4506 this.selectors.productThumbImages + 4507 "[data-thumbnail-id='" + 4508 imageId + 4509 "']" 4510 ); 4511 4512 $(this.selectors.productThumbImages) 4513 .removeClass(this.classes.activeClass) 4514 .removeAttr('aria-current'); 4515 4516 $activeThumbnail.addClass(this.classes.activeClass); 4517 $activeThumbnail.attr('aria-current', true); 4518 4519 if (!$thumbnailWrappers.hasClass('slick-slide')) { 4520 return; 4521 } 4522 4523 var slideIndex = $activeThumbnail.parent().data('slick-index'); 4524 4525 $(this.selectors.productThumbs).slick('slickGoTo', slideIndex, true); 4526 }, 4527 4528 _switchImage: function(imageId) { 4529 var $newImage = $( 4530 this.selectors.productImageWraps + "[data-image-id='" + imageId + "']", 4531 this.$container 4532 ); 4533 var $otherImages = $( 4534 this.selectors.productImageWraps + 4535 ":not([data-image-id='" + 4536 imageId + 4537 "'])", 4538 this.$container 4539 ); 4540 4541 $newImage.removeClass(this.classes.hidden); 4542 $otherImages.addClass(this.classes.hidden); 4543 }, 4544 4545 _handleImageFocus: function(evt) { 4546 if (evt.keyCode !== slate.utils.keyboardKeys.ENTER) return; 4547 4548 $(this.selectors.productFeaturedImage + ':visible').focus(); 4549 }, 4550 4551 _initThumbnailSlider: function() { 4552 var options = { 4553 slidesToShow: 4, 4554 slidesToScroll: 3, 4555 infinite: false, 4556 prevArrow: '.thumbnails-slider__prev--' + this.settings.sectionId,
4557 nextArrow: '.thumbnails-slider__next--' + this.settings.sectionId, 4558 responsive: [ 4559 { 4560 breakpoint: 321, 4561 settings: { 4562 slidesToShow: 3 4563 } 4564 } 4565 ] 4566 }; 4567 4568 $(this.selectors.productThumbs).slick(options); 4569 4570 // Accessibility concerns not yet fixed in Slick Slider 4571 $(this.selectors.productThumbsWrapper, this.$container) 4572 .find('.slick-list') 4573 .removeAttr('aria-live'); 4574 $(this.selectors.productThumbsWrapper, this.$container) 4575 .find('.slick-disabled') 4576 .removeAttr('aria-disabled'); 4577 4578 this.settings.sliderActive = true; 4579 }, 4580 4581 _destroyThumbnailSlider: function() { 4582 $(this.selectors.productThumbs).slick('unslick'); 4583 this.settings.sliderActive = false; 4584 4585 // Accessibility concerns not yet fixed in Slick Slider 4586 $(this.selectors.productThumbsWrapper, this.$container) 4587 .find('[tabindex="-1"]') 4588 .removeAttr('tabindex'); 4589 }, 4590 4591 _liveRegionText: function(variant) { 4592 // Dummy content for live region 4593 var liveRegionText = 4594 '[Availability] [Regular] [$$] [Sale] [$]. [UnitPrice] [$$$]'; 4595 4596 if (!variant) { 4597 liveRegionText = theme.strings.unavailable; 4598 return liveRegionText; 4599 } 4600 4601 // Update availability 4602 var availability = variant.available ? '' : theme.strings.soldOut + ','; 4603 liveRegionText = liveRegionText.replace('[Availability]', availability); 4604 4605 // Update pricing information 4606 var regularLabel = ''; 4607 var regularPrice = theme.Currency.formatMoney( 4608 variant.price, 4609 theme.moneyFormat 4610 ); 4611 var saleLabel = ''; 4612 var salePrice = ''; 4613 var unitLabel = ''; 4614 var unitPrice = ''; 4615 4616 if (variant.compare_at_price > variant.price) { 4617 regularLabel = theme.strings.regularPrice; 4618 regularPrice = 4619 theme.Currency.formatMoney( 4620 variant.compare_at_price, 4621 theme.moneyFormat 4622 ) + ','; 4623 saleLabel = theme.strings.sale; 4624 salePrice = theme.Currency.formatMoney( 4625 variant.price, 4626 theme.moneyFormat 4627 ); 4628 } 4629 4630 if (variant.unit_price) { 4631 unitLabel = theme.strings.unitPrice; 4632 unitPrice = 4633 theme.Currency.formatMoney(variant.unit_price, theme.moneyFormat) + 4634 ' ' + 4635 theme.strings.unitPriceSeparator + 4636 ' ' + 4637 this._getBaseUnit(variant); 4638 } 4639 4640 liveRegionText = liveRegionText 4641 .replace('[Regular]', regularLabel) 4642 .replace('[$$]', regularPrice) 4643 .replace('[Sale]', saleLabel) 4644 .replace('[$]', salePrice) 4645 .replace('[UnitPrice]', unitLabel) 4646 .replace('[$$$]', unitPrice) 4647 .trim(); 4648 4649 return liveRegionText; 4650 }, 4651 4652 _updateLiveRegion: function(evt) { 4653 var variant = evt.variant; 4654 var liveRegion = this.container.querySelector( 4655 this.selectors.productStatus 4656 ); 4657 liveRegion.innerHTML = this._liveRegionText(variant); 4658 liveRegion.setAttribute('aria-hidden', false); 4659 4660 // hide content from accessibility tree after announcement 4661 setTimeout(function() { 4662 liveRegion.setAttribute('aria-hidden', true); 4663 }, 1000); 4664 }, 4665 4666 _updateAddToCart: function(evt) { 4667 var variant = evt.variant; 4668 4669 if (variant) { 4670 if (variant.available) { 4671 this.$addToCart 4672 .removeAttr('aria-disabled') 4673 .attr('aria-label', theme.strings.addToCart); 4674 $(this.selectors.addToCartText, this.$container).text( 4675 theme.strings.addToCart 4676 ); 4677 this.$shopifyPaymentButton.show(); 4678 } else { 4679 // The variant doesn't exist, disable submit button and change the text. 4680 // This may be an error or notice that a specific variant is not available. 4681 this.$addToCart 4682 .attr('aria-disabled', true) 4683 .attr('aria-label', theme.strings.soldOut); 4684 $(this.selectors.addToCartText, this.$container).text( 4685 theme.strings.soldOut 4686 ); 4687 this.$shopifyPaymentButton.hide(); 4688 } 4689 } else { 4690 this.$addToCart 4691 .attr('aria-disabled', true) 4692 .attr('aria-label', theme.strings.unavailable); 4693 $(this.selectors.addToCartText, this.$container).text( 4694 theme.strings.unavailable 4695 ); 4696 this.$shopifyPaymentButton.hide(); 4697 } 4698 }, 4699 4700 _updateAvailability: function(evt) { 4701 // remove error message if one is showing 4702 this._hideErrorMessage(); 4703 4704 // update form submit 4705 this._updateAddToCart(evt); 4706 // update live region 4707 this._updateLiveRegion(evt); 4708 4709 this._updatePrice(evt); 4710 }, 4711 4712 _updateImages: function(evt) { 4713 var variant = evt.variant; 4714 var imageId = variant.featured_image.id; 4715 4716 this._switchImage(imageId);
4717 this._setActiveThumbnail(imageId); 4718 }, 4719 4720 _updatePrice: function(evt) { 4721 var variant = evt.variant; 4722 4723 var $priceContainer = $(this.selectors.priceContainer, this.$container); 4724 var $regularPrice = $(this.selectors.regularPrice, $priceContainer); 4725 var $salePrice = $(this.selectors.salePrice, $priceContainer); 4726 var $unitPrice = $(this.selectors.unitPrice, $priceContainer); 4727 var $unitPriceBaseUnit = $( 4728 this.selectors.unitPriceBaseUnit, 4729 $priceContainer 4730 ); 4731 4732 // Reset product price state 4733 $priceContainer 4734 .removeClass(this.classes.productUnavailable) 4735 .removeClass(this.classes.productOnSale) 4736 .removeClass(this.classes.productUnitAvailable) 4737 .removeAttr('aria-hidden'); 4738 4739 // Unavailable 4740 if (!variant) { 4741 $priceContainer 4742 .addClass(this.classes.productUnavailable) 4743 .attr('aria-hidden', true); 4744 return; 4745 } 4746 4747 // On sale 4748 if (variant.compare_at_price > variant.price) { 4749 $regularPrice.html( 4750 theme.Currency.formatMoney( 4751 variant.compare_at_price, 4752 theme.moneyFormat 4753 ) 4754 ); 4755 $salePrice.html( 4756 theme.Currency.formatMoney(variant.price, theme.moneyFormat) 4757 ); 4758 $priceContainer.addClass(this.classes.productOnSale); 4759 } else { 4760 // Regular price 4761 $regularPrice.html( 4762 theme.Currency.formatMoney(variant.price, theme.moneyFormat) 4763 ); 4764 } 4765 4766 // Unit price 4767 if (variant.unit_price) { 4768 $unitPrice.html( 4769 theme.Currency.formatMoney(variant.unit_price, theme.moneyFormat) 4770 ); 4771 $unitPriceBaseUnit.html(this._getBaseUnit(variant)); 4772 $priceContainer.addClass(this.classes.productUnitAvailable); 4773 } 4774 }, 4775 4776 _getBaseUnit: function(variant) { 4777 return variant.unit_price_measurement.reference_value === 1 4778 ? variant.unit_price_measurement.reference_unit 4779 : variant.unit_price_measurement.reference_value + 4780 variant.unit_price_measurement.reference_unit; 4781 }, 4782 4783 _updateSKU: function(evt) { 4784 var variant = evt.variant; 4785 4786 // Update the sku 4787 $(this.selectors.SKU).html(variant.sku); 4788 }, 4789 4790 onUnload: function() { 4791 this.$container.off(this.settings.namespace); 4792 } 4793 }); 4794 4795 function _enableZoom(el) { 4796 var zoomUrl = $(el).data('zoom'); 4797 $(el).zoom({ 4798 url: zoomUrl 4799 }); 4800 } 4801 4802 function _destroyZoom(el) { 4803 $(el).trigger('zoom.destroy'); 4804 } 4805 4806 return Product; 4807})(); 4808 4809theme.ProductRecommendations = (function() { 4810 function ProductRecommendations(container) { 4811 this.$container = $(container); 4812 4813 var productId = this.$container.data('productId'); 4814 var recommendationsSectionUrl = 4815 '/recommendations/products?§ion_id=product-recommendations&product_id=' + 4816 productId + 4817 '&limit=4'; 4818 4819 $.get(recommendationsSectionUrl).then( 4820 function(section) { 4821 var recommendationsMarkup = $(section).html(); 4822 if (recommendationsMarkup.trim() !== '') { 4823 this.$container.html(recommendationsMarkup); 4824 } 4825 }.bind(this) 4826 ); 4827 } 4828 4829 return ProductRecommendations; 4830})(); 4831 4832theme.Quotes = (function() { 4833 var config = { 4834 mediaQuerySmall: 'screen and (max-width: 749px)', 4835 mediaQueryMediumUp: 'screen and (min-width: 750px)', 4836 slideCount: 0 4837 }; 4838 var defaults = { 4839 accessibility: true, 4840 arrows: false, 4841 dots: true, 4842 autoplay: false, 4843 touchThreshold: 20, 4844 slidesToShow: 3, 4845 slidesToScroll: 3 4846 }; 4847 4848 function Quotes(container) { 4849 var $container = (this.$container = $(container)); 4850 var sectionId = $container.attr('data-section-id'); 4851 var wrapper = (this.wrapper = '.quotes-wrapper'); 4852 var slider = (this.slider = '#Quotes-' + sectionId); 4853 var $slider = $(slider, wrapper); 4854 4855 var sliderActive = false; 4856 var mobileOptions = $.extend({}, defaults, { 4857 slidesToShow: 1, 4858 slidesToScroll: 1, 4859 adaptiveHeight: true 4860 }); 4861 4862 config.slideCount = $slider.data('count'); 4863 4864 // Override slidesToShow/Scroll if there are not enough blocks 4865 if (config.slideCount < defaults.slidesToShow) { 4866 defaults.slidesToShow = config.slideCount; 4867 defaults.slidesToScroll = config.slideCount; 4868 } 4869 4870 $slider.on('init', this.a11y.bind(this)); 4871 4872 enquire.register(config.mediaQuerySmall, { 4873 match: function() { 4874 initSlider($slider, mobileOptions); 4875 } 4876 }); 4877 4878 enquire.register(config.mediaQueryMediumUp, { 4879 match: function() { 4880 initSlider($slider, defaults); 4881 } 4882 }); 4883 4884 function initSlider(sliderObj, args) { 4885 if (sliderActive) { 4886 sliderObj.slick('unslick'); 4887 sliderActive = false;
4888 } 4889 4890 sliderObj.slick(args); 4891 sliderActive = true; 4892 } 4893 } 4894 4895 Quotes.prototype = _.assignIn({}, Quotes.prototype, { 4896 onUnload: function() { 4897 enquire.unregister(config.mediaQuerySmall); 4898 enquire.unregister(config.mediaQueryMediumUp); 4899 4900 $(this.slider, this.wrapper).slick('unslick'); 4901 }, 4902 4903 onBlockSelect: function(evt) { 4904 // Ignore the cloned version 4905 var $slide = $( 4906 '.quotes-slide--' + evt.detail.blockId + ':not(.slick-cloned)' 4907 ); 4908 var slideIndex = $slide.data('slick-index'); 4909 4910 // Go to selected slide, pause autoplay 4911 $(this.slider, this.wrapper).slick('slickGoTo', slideIndex); 4912 }, 4913 4914 a11y: function(event, obj) { 4915 var $list = obj.$list; 4916 var $wrapper = $(this.wrapper, this.$container); 4917 4918 // Remove default Slick aria-live attr until slider is focused 4919 $list.removeAttr('aria-live'); 4920 4921 // When an element in the slider is focused set aria-live 4922 $wrapper.on('focusin', function(evt) { 4923 if ($wrapper.has(evt.target).length) { 4924 $list.attr('aria-live', 'polite'); 4925 } 4926 }); 4927 4928 // Remove aria-live 4929 $wrapper.on('focusout', function(evt) { 4930 if ($wrapper.has(evt.target).length) { 4931 $list.removeAttr('aria-live'); 4932 } 4933 }); 4934 } 4935 }); 4936 4937 return Quotes; 4938})(); 4939 4940theme.slideshows = {}; 4941 4942theme.SlideshowSection = (function() { 4943 function SlideshowSection(container) { 4944 var $container = (this.$container = $(container)); 4945 var sectionId = $container.attr('data-section-id'); 4946 var slideshow = (this.slideshow = '#Slideshow-' + sectionId); 4947 4948 theme.slideshows[slideshow] = new theme.Slideshow(slideshow, sectionId); 4949 } 4950 4951 return SlideshowSection; 4952})(); 4953 4954theme.SlideshowSection.prototype = _.assignIn( 4955 {}, 4956 theme.SlideshowSection.prototype, 4957 { 4958 onUnload: function() { 4959 delete theme.slideshows[this.slideshow]; 4960 }, 4961 4962 onBlockSelect: function(evt) { 4963 var $slideshow = $(this.slideshow); 4964 var adaptHeight = $slideshow.data('adapt-height'); 4965 4966 if (adaptHeight) { 4967 theme.slideshows[this.slideshow].setSlideshowHeight(); 4968 } 4969 4970 // Ignore the cloned version 4971 var $slide = $( 4972 '.slideshow__slide--' + evt.detail.blockId + ':not(.slick-cloned)' 4973 ); 4974 var slideIndex = $slide.data('slick-index'); 4975 4976 // Go to selected slide, pause auto-rotate 4977 $slideshow.slick('slickGoTo', slideIndex).slick('slickPause'); 4978 }, 4979 4980 onBlockDeselect: function() { 4981 // Resume auto-rotate 4982 $(this.slideshow).slick('slickPlay'); 4983 } 4984 } 4985); 4986 4987theme.slideshows = {}; 4988 4989theme.VideoSection = (function() { 4990 function VideoSection(container) { 4991 var $container = (this.$container = $(container)); 4992 4993 $('.video', $container).each(function() { 4994 var $el = $(this); 4995 theme.Video.init($el); 4996 theme.Video.editorLoadVideo($el.attr('id')); 4997 }); 4998 } 4999 5000 return VideoSection; 5001})(); 5002 5003theme.VideoSection.prototype = _.assignIn({}, theme.VideoSection.prototype, { 5004 onUnload: function() { 5005 theme.Video.removeEvents(); 5006 } 5007}); 5008 5009theme.heros = {}; 5010 5011theme.HeroSection = (function() { 5012 function HeroSection(container) { 5013 var $container = (this.$container = $(container)); 5014 var sectionId = $container.attr('data-section-id'); 5015 var hero = '#Hero-' + sectionId; 5016 theme.heros[hero] = new theme.Hero(hero, sectionId); 5017 } 5018 5019 return HeroSection; 5020})(); 5021 5022 5023$(document).ready(function() { 5024 var sections = new theme.Sections(); 5025 5026 sections.register('cart-template', theme.Cart); 5027 sections.register('product', theme.Product); 5028 sections.register('collection-template', theme.Filters); 5029 sections.register('product-template', theme.Product); 5030 sections.register('header-section', theme.HeaderSection); 5031 sections.register('map', theme.Maps); 5032 sections.register('slideshow-section', theme.SlideshowSection); 5033 sections.register('video-section', theme.VideoSection); 5034 sections.register('quotes', theme.Quotes); 5035 sections.register('hero-section', theme.HeroSection); 5036 sections.register('product-recommendations', theme.ProductRecommendations); 5037}); 5038 5039theme.init = function() { 5040 theme.customerTemplates.init(); 5041 5042 // Theme-specific selectors to make tables scrollable 5043 var tableSelectors = '.rte table,' + '.custom__item-inner--html table'; 5044 5045 slate.rte.wrapTable({ 5046 $tables: $(tableSelectors), 5047 tableWrapperClass: 'scrollable-wrapper' 5048 }); 5049 5050 // Theme-specific selectors to make iframes responsive 5051 var iframeSelectors = 5052 '.rte iframe[src*="youtube.com/embed"],' + 5053 '.rte iframe[src*="player.vimeo"],' + 5054 '.custom__item-inner--html iframe[src*="youtube.com/embed"],' + 5055 '.custom__item-inner--html iframe[src*="player.vimeo"]'; 5056 5057 slate.rte.wrapIframe({ 5058 $iframes: $(iframeSelectors), 5059 iframeWrapperClass: 'video-wrapper' 5060 }); 5061 5062 // Common a11y fixes 5063 slate.a11y.pageLinkFocus($(window.location.hash)); 5064 5065 $('.in-page-link').on('click', function(evt) { 5066 slate.a11y.pageLinkFocus($(evt.currentTarget.hash)); 5067 }); 5068 5069 $('a[href="#"]').on('click', function(evt) { 5070 evt.preventDefault(); 5071 }); 5072 5073 slate.a11y.accessibleLinks({ 5074 messages: { 5075 newWindow: theme.strings.newWindow, 5076 external: theme.strings.external, 5077 newWindowExternal: theme.strings.newWindowExternal 5078 }, 5079 $links: $('a[href]:not([aria-describedby], .product-single__thumbnail)') 5080 }); 5081 5082 theme.FormStatus.init(); 5083 5084 var selectors = { 5085 image: '[data-image]', 5086 imagePlaceholder: '[data-image-placeholder]', 5087 imageWithPlaceholderWrapper: '[data-image-with-placeholder-wrapper]' 5088 }; 5089 5090 var classes = { 5091 hidden: 'hide' 5092 }; 5093 5094 $(document).on('lazyloaded', function(e) { 5095 var $target = $(e.target); 5096 5097 if (!$target.is(selectors.image)) { 5098 return; 5099 } 5100 5101 $target 5102 .closest(selectors.imageWithPlaceholderWrapper) 5103 .find(selectors.imagePlaceholder)
5104 .addClass(classes.hidden); 5105 }); 5106 5107 // When the theme loads, lazysizes might load images before the "lazyloaded" 5108 // event listener has been attached. When this happens, the following function 5109 // hides the loading placeholders. 5110 function onLoadHideLazysizesAnimation() { 5111 $(selectors.image + '.lazyloaded') 5112 .closest(selectors.imageWithPlaceholderWrapper) 5113 .find(selectors.imagePlaceholder) 5114 .addClass(classes.hidden); 5115 } 5116 5117 onLoadHideLazysizesAnimation(); 5118}; 5119 5120$(theme.init);
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.