Advertisement
cowfee

Untitled

Feb 16th, 2020
1,303
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 209.39 KB | None | 0 0
  1. // Use our custom event listeners to tap into common functions.
  2. // Documentation - https://archetypethemes.co/blogs/streamline/javascript-events-for-developers
  3. //
  4. // document.addEventListener('page:loaded', function() {
  5. // /* Stylesheet and theme scripts have loaded */
  6. // });
  7.  
  8. window.theme = window.theme || {};
  9. window.slate = window.slate || {};
  10.  
  11. if (console && console.log) {
  12. console.log('Streamline theme ('+theme.settings.themeVersion+') by ARCHΞTYPE | Learn more at https://archetypethemes.co');
  13. }
  14.  
  15. window.lazySizesConfig = window.lazySizesConfig || {};
  16. lazySizesConfig.expFactor = 4;
  17.  
  18. (function($){
  19. var $ = jQuery = $;
  20.  
  21. slate.utils = {
  22. /**
  23. * _.defaultTo from lodash
  24. * Checks `value` to determine whether a default value should be returned in
  25. * its place. The `defaultValue` is returned if `value` is `NaN`, `null`,
  26. * or `undefined`.
  27. * Source: https://github.com/lodash/lodash/blob/master/defaultTo.js
  28. *
  29. * @param {*} value - Value to check
  30. * @param {*} defaultValue - Default value
  31. * @returns {*} - Returns the resolved value
  32. */
  33. defaultTo: function(value, defaultValue) {
  34. return (value == null || value !== value) ? defaultValue : value
  35. },
  36.  
  37. promiseStylesheet: function() {
  38. if (typeof this.stylesheetPromise === 'undefined') {
  39. this.stylesheetPromise = $.Deferred(function(defer) {
  40. var link = document.querySelector('link[href="' + theme.stylesheet + '"]');
  41.  
  42. if (link.loaded) {
  43. defer.resolve();
  44. }
  45.  
  46. onloadCSS(link, function() { // Global onloadCSS function injected by load-css.liquid
  47. defer.resolve();
  48. });
  49. });
  50. }
  51.  
  52. return this.stylesheetPromise;
  53. }
  54. };
  55.  
  56. slate.a11y = {
  57.  
  58. /**
  59. * Traps the focus in a particular container
  60. *
  61. * @param {object} options - Options to be used
  62. * @param {jQuery} options.$container - Container to trap focus within
  63. * @param {jQuery} options.$elementToFocus - Element to be focused when focus leaves container
  64. * @param {string} options.namespace - Namespace used for new focus event handler
  65. */
  66. trapFocus: function(options) {
  67. var eventName = options.namespace
  68. ? 'focusin.' + options.namespace
  69. : 'focusin';
  70.  
  71. if (!options.$elementToFocus) {
  72. options.$elementToFocus = options.$container;
  73. }
  74.  
  75. options.$container.attr('tabindex', '-1');
  76. options.$elementToFocus.focus();
  77.  
  78. $(document).off('focusin');
  79.  
  80. $(document).on(eventName, function(evt) {
  81. if (options.$container[0] !== evt.target && !options.$container.has(evt.target).length) {
  82. options.$container.focus();
  83. }
  84. });
  85. },
  86.  
  87. /**
  88. * Removes the trap of focus in a particular container
  89. *
  90. * @param {object} options - Options to be used
  91. * @param {jQuery} options.$container - Container to trap focus within
  92. * @param {string} options.namespace - Namespace used for new focus event handler
  93. */
  94. removeTrapFocus: function(options) {
  95. var eventName = options.namespace
  96. ? 'focusin.' + options.namespace
  97. : 'focusin';
  98.  
  99. if (options.$container && options.$container.length) {
  100. options.$container.removeAttr('tabindex');
  101. }
  102.  
  103. $(document).off(eventName);
  104. },
  105.  
  106. // Not from Slate, but fit in the a11y category
  107. lockMobileScrolling: function(namespace, $element) {
  108. if ($element) {
  109. var $el = $element;
  110. } else {
  111. var $el = $(document.documentElement).add('body');
  112. }
  113. $el.on('touchmove' + namespace, function () {
  114. return false;
  115. });
  116. },
  117.  
  118. unlockMobileScrolling: function(namespace, $element) {
  119. if ($element) {
  120. var $el = $element;
  121. } else {
  122. var $el = $(document.documentElement).add('body');
  123. }
  124. $el.off(namespace);
  125. },
  126.  
  127. promiseAnimationEnd: function($el) {
  128. var events = 'animationend webkitAnimationEnd oAnimationEnd';
  129. var properties = ['animation-duration', '-moz-animation-duration', '-webkit-animation-duration', '-o-animation-duration'];
  130. var duration = 0;
  131. var promise = $.Deferred().resolve();
  132.  
  133. // check the various CSS properties to see if a duration has been set
  134. $.each(properties, function(index, value) {
  135. duration || (duration = parseFloat($el.css(value)));
  136. });
  137.  
  138. if (duration > 0) {
  139. promise = $.Deferred(function(defer) {
  140. $el.on(events, function(evt) {
  141. if (evt.target !== $el[0]) return;
  142. $el.off(events);
  143. defer.resolve();
  144. });
  145. });
  146. }
  147.  
  148. return promise;
  149. },
  150.  
  151. promiseTransitionEnd: function($el) {
  152. var events = 'webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend';
  153. var properties = ['transition-duration', '-moz-transition-duration', '-webkit-transition-duration', '-o-transition-duration'];
  154. var duration = 0;
  155. var promise = $.Deferred().resolve();
  156.  
  157. // check the various CSS properties to see if a duration has been set
  158. $.each(properties, function(index, value) {
  159. duration || (duration = parseFloat($el.css(value)));
  160. });
  161.  
  162. if (duration > 0) {
  163. promise = $.Deferred(function(defer) {
  164. $el.on(events, function(evt) {
  165. if (evt.target !== $el[0]) return;
  166. $el.off(events);
  167. defer.resolve();
  168. });
  169. });
  170. }
  171.  
  172. return promise;
  173. }
  174. };
  175.  
  176. theme.Sections = function Sections() {
  177. this.constructors = {};
  178. this.instances = [];
  179.  
  180. $(document)
  181. .on('shopify:section:load', this._onSectionLoad.bind(this))
  182. .on('shopify:section:unload', this._onSectionUnload.bind(this))
  183. .on('shopify:section:select', this._onSelect.bind(this))
  184. .on('shopify:section:deselect', this._onDeselect.bind(this))
  185. .on('shopify:block:select', this._onBlockSelect.bind(this))
  186. .on('shopify:block:deselect', this._onBlockDeselect.bind(this));
  187. };
  188.  
  189. theme.Sections.prototype = $.extend({}, theme.Sections.prototype, {
  190. createInstance: function(container, constructor, customScope) {
  191. var $container = $(container);
  192. var id = $container.attr('data-section-id');
  193. var type = $container.attr('data-section-type');
  194.  
  195. constructor = constructor || this.constructors[type];
  196.  
  197. if (typeof constructor === 'undefined') {
  198. return;
  199. }
  200.  
  201. // If custom scope passed, check to see if instance
  202. // is already initialized so we don't double up
  203. if (customScope) {
  204. var instanceExists = this._findInstance(id);
  205. if (instanceExists) {
  206. return;
  207. }
  208. }
  209.  
  210. var instance = $.extend(new constructor(container), {
  211. id: id,
  212. type: type,
  213. container: container,
  214. namespace: '.' + type + '-' + id
  215. });
  216.  
  217. this.instances.push(instance);
  218. },
  219.  
  220. _onSectionLoad: function(evt, subSection, subSectionId) {
  221. if (AOS) {
  222. AOS.refreshHard();
  223. }
  224.  
  225. var container = subSection ? subSection : $('[data-section-id]', evt.target)[0];
  226.  
  227. if (!container) {
  228. return;
  229. }
  230.  
  231. this.createInstance(container);
  232.  
  233. var instance = subSection ? subSectionId : this._findInstance(evt.detail.sectionId);
  234.  
  235. if (!subSection) {
  236. this.loadSubSections();
  237. }
  238.  
  239. // Run JS only in case of the section being selected in the editor
  240. // before merchant clicks "Add"
  241. if (instance && typeof instance.onLoad === 'function') {
  242. instance.onLoad(evt);
  243. }
  244. },
  245.  
  246. loadSubSections: function($context) {
  247. var $sections = $context ? $context.find('[data-subsection]') : $('[data-subsection]');
  248.  
  249. $sections.each(function(evt, el) {
  250. this._onSectionLoad(null, el, $(el).data('section-id'));
  251. }.bind(this));
  252.  
  253. if (AOS) {
  254. AOS.refreshHard();
  255. }
  256. },
  257.  
  258. _onSectionUnload: function(evt) {
  259. var instance = this._removeInstance(evt.detail.sectionId);
  260. if (instance && typeof instance.onUnload === 'function') {
  261. instance.onUnload(evt);
  262. }
  263. },
  264.  
  265. _onSelect: function(evt) {
  266. var instance = this._findInstance(evt.detail.sectionId);
  267.  
  268. if (instance && typeof instance.onSelect === 'function') {
  269. instance.onSelect(evt);
  270. }
  271. },
  272.  
  273. _onDeselect: function(evt) {
  274. var instance = this._findInstance(evt.detail.sectionId);
  275.  
  276. if (instance && typeof instance.onDeselect === 'function') {
  277. instance.onDeselect(evt);
  278. }
  279. },
  280.  
  281. _onBlockSelect: function(evt) {
  282. var instance = this._findInstance(evt.detail.sectionId);
  283.  
  284. if (instance && typeof instance.onBlockSelect === 'function') {
  285. instance.onBlockSelect(evt);
  286. }
  287. },
  288.  
  289. _onBlockDeselect: function(evt) {
  290. var instance = this._findInstance(evt.detail.sectionId);
  291.  
  292. if (instance && typeof instance.onBlockDeselect === 'function') {
  293. instance.onBlockDeselect(evt);
  294. }
  295. },
  296.  
  297. _findInstance: function(id) {
  298. for (var i = 0; i < this.instances.length; i++) {
  299. if (this.instances[i].id === id) {
  300. return this.instances[i];
  301. }
  302. }
  303. },
  304.  
  305. _removeInstance: function(id) {
  306. var i = this.instances.length;
  307. var instance;
  308.  
  309. while(i--) {
  310. if (this.instances[i].id === id) {
  311. instance = this.instances[i];
  312. this.instances.splice(i, 1);
  313. break;
  314. }
  315. }
  316.  
  317. return instance;
  318. },
  319.  
  320. reinitSection: function(section) {
  321. for (var i = 0; i < sections.instances.length; i++) {
  322. var instance = sections.instances[i];
  323. if (instance['type'] === section) {
  324. if (typeof instance.forceReload === 'function') {
  325. instance.forceReload();
  326. }
  327. }
  328. }
  329. },
  330.  
  331. register: function(type, constructor, $scope) {
  332. var afterLoad = false;
  333. this.constructors[type] = constructor;
  334. var $sections = $('[data-section-type=' + type + ']');
  335.  
  336. // Any section within the scope
  337. if ($scope) {
  338. $sections = $('[data-section-type=' + type + ']', $scope);
  339. }
  340.  
  341. $sections.each(function(index, container) {
  342. this.createInstance(container, constructor, $scope);
  343. }.bind(this));
  344. }
  345. });
  346.  
  347. theme.Currency = (function() {
  348. var moneyFormat = '${{amount}}';
  349.  
  350. function formatMoney(cents, format) {
  351. if (typeof cents === 'string') {
  352. cents = cents.replace('.', '');
  353. }
  354. var value = '';
  355. var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/;
  356. var formatString = (format || moneyFormat);
  357.  
  358. function formatWithDelimiters(number, precision, thousands, decimal) {
  359. precision = slate.utils.defaultTo(precision, 2);
  360. thousands = slate.utils.defaultTo(thousands, ',');
  361. decimal = slate.utils.defaultTo(decimal, '.');
  362.  
  363. if (isNaN(number) || number == null) {
  364. return 0;
  365. }
  366.  
  367. number = (number / 100.0).toFixed(precision);
  368.  
  369. var parts = number.split('.');
  370. var dollarsAmount = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + thousands);
  371. var centsAmount = parts[1] ? (decimal + parts[1]) : '';
  372.  
  373. return dollarsAmount + centsAmount;
  374. }
  375.  
  376. switch (formatString.match(placeholderRegex)[1]) {
  377. case 'amount':
  378. value = formatWithDelimiters(cents, 2);
  379. break;
  380. case 'amount_no_decimals':
  381. value = formatWithDelimiters(cents, 0);
  382. break;
  383. case 'amount_with_comma_separator':
  384. value = formatWithDelimiters(cents, 2, '.', ',');
  385. break;
  386. case 'amount_no_decimals_with_comma_separator':
  387. value = formatWithDelimiters(cents, 0, '.', ',');
  388. break;
  389. case 'amount_no_decimals_with_space_separator':
  390. value = formatWithDelimiters(cents, 0, ' ');
  391. break;
  392. }
  393.  
  394. return formatString.replace(placeholderRegex, value);
  395. }
  396.  
  397. function getBaseUnit(variant) {
  398. if (!variant) {
  399. return;
  400. }
  401.  
  402. if (!variant.unit_price_measurement || !variant.unit_price_measurement.reference_value) {
  403. return;
  404. }
  405.  
  406. return variant.unit_price_measurement.reference_value === 1
  407. ? variant.unit_price_measurement.reference_unit
  408. : variant.unit_price_measurement.reference_value +
  409. variant.unit_price_measurement.reference_unit;
  410. }
  411.  
  412. return {
  413. formatMoney: formatMoney,
  414. getBaseUnit: getBaseUnit
  415. }
  416. })();
  417.  
  418.  
  419. /**
  420. * Image Helper Functions
  421. * -----------------------------------------------------------------------------
  422. * A collection of functions that help with basic image operations.
  423. *
  424. */
  425.  
  426. theme.Images = (function() {
  427.  
  428. /**
  429. * Find the Shopify image attribute size
  430. *
  431. * @param {string} src
  432. * @returns {null}
  433. */
  434. function imageSize(src) {
  435. if (!src) {
  436. return '620x'; // default based on theme
  437. }
  438.  
  439. var match = src.match(/.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\.@]/);
  440.  
  441. if (match !== null) {
  442. return match[1];
  443. } else {
  444. return null;
  445. }
  446. }
  447.  
  448. /**
  449. * Adds a Shopify size attribute to a URL
  450. *
  451. * @param src
  452. * @param size
  453. * @returns {*}
  454. */
  455. function getSizedImageUrl(src, size) {
  456. if (size == null) {
  457. return src;
  458. }
  459.  
  460. if (size === 'master') {
  461. return this.removeProtocol(src);
  462. }
  463.  
  464. var match = src.match(/\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i);
  465.  
  466. if (match != null) {
  467. var prefix = src.split(match[0]);
  468. var suffix = match[0];
  469.  
  470. return this.removeProtocol(prefix[0] + '_' + size + suffix);
  471. }
  472.  
  473. return null;
  474. }
  475.  
  476. function removeProtocol(path) {
  477. return path.replace(/http(s)?:/, '');
  478. }
  479.  
  480. return {
  481. imageSize: imageSize,
  482. getSizedImageUrl: getSizedImageUrl,
  483. removeProtocol: removeProtocol
  484. };
  485. })();
  486.  
  487. slate.Variants = (function() {
  488.  
  489. function Variants(options) {
  490. this.$container = options.$container;
  491. this.variants = options.variants;
  492. this.singleOptionSelector = options.singleOptionSelector;
  493. this.originalSelectorId = options.originalSelectorId;
  494. this.enableHistoryState = options.enableHistoryState;
  495. this.currentVariant = this._getVariantFromOptions();
  496.  
  497. $(this.singleOptionSelector, this.$container).on('change', this._onSelectChange.bind(this));
  498. }
  499.  
  500. Variants.prototype = $.extend({}, Variants.prototype, {
  501.  
  502. _getCurrentOptions: function() {
  503. var currentOptions = $.map($(this.singleOptionSelector, this.$container), function(element) {
  504. var $element = $(element);
  505. var type = $element.attr('type');
  506. var currentOption = {};
  507.  
  508. if (type === 'radio' || type === 'checkbox') {
  509. if ($element[0].checked) {
  510. currentOption.value = $element.val();
  511. currentOption.index = $element.data('index');
  512.  
  513. return currentOption;
  514. } else {
  515. return false;
  516. }
  517. } else {
  518. currentOption.value = $element.val();
  519. currentOption.index = $element.data('index');
  520.  
  521. return currentOption;
  522. }
  523. });
  524.  
  525. // remove any unchecked input values if using radio buttons or checkboxes
  526. currentOptions = this._compact(currentOptions);
  527.  
  528. return currentOptions;
  529. },
  530.  
  531. _getVariantFromOptions: function() {
  532. var selectedValues = this._getCurrentOptions();
  533. var variants = this.variants;
  534. var found = false;
  535.  
  536. variants.forEach(function(variant) {
  537. var match = true;
  538. var options = variant.options;
  539.  
  540. selectedValues.forEach(function(option) {
  541. if (match) {
  542. match = (variant[option.index] === option.value);
  543. }
  544. });
  545.  
  546. if (match) {
  547. found = variant;
  548. }
  549. });
  550.  
  551. return found || null;
  552. },
  553.  
  554. _onSelectChange: function() {
  555. var variant = this._getVariantFromOptions();
  556.  
  557. this.$container.trigger({
  558. type: 'variantChange',
  559. variant: variant
  560. });
  561.  
  562. document.dispatchEvent(new CustomEvent('variant:change', {
  563. detail: {
  564. variant: variant
  565. }
  566. }));
  567.  
  568. if (!variant) {
  569. return;
  570. }
  571.  
  572. this._updateMasterSelect(variant);
  573. this._updateImages(variant);
  574. this._updatePrice(variant);
  575. this._updateUnitPrice(variant);
  576. this._updateSKU(variant);
  577. this.currentVariant = variant;
  578.  
  579. if (this.enableHistoryState) {
  580. this._updateHistoryState(variant);
  581. }
  582. },
  583.  
  584. _updateImages: function(variant) {
  585. var variantImage = variant.featured_image || {};
  586. var currentVariantImage = this.currentVariant.featured_image || {};
  587.  
  588. if (!variant.featured_image || variantImage.src === currentVariantImage.src) {
  589. return;
  590. }
  591.  
  592. this.$container.trigger({
  593. type: 'variantImageChange',
  594. variant: variant
  595. });
  596. },
  597.  
  598. _updatePrice: function(variant) {
  599. if (variant.price === this.currentVariant.price && variant.compare_at_price === this.currentVariant.compare_at_price) {
  600. return;
  601. }
  602.  
  603. this.$container.trigger({
  604. type: 'variantPriceChange',
  605. variant: variant
  606. });
  607. },
  608.  
  609. _updateUnitPrice: function(variant) {
  610. if (variant.unit_price === this.currentVariant.unit_price) {
  611. return;
  612. }
  613.  
  614. this.$container.trigger({
  615. type: 'variantUnitPriceChange',
  616. variant: variant
  617. });
  618. },
  619.  
  620. _updateSKU: function(variant) {
  621. if (variant.sku === this.currentVariant.sku) {
  622. return;
  623. }
  624.  
  625. this.$container.trigger({
  626. type: 'variantSKUChange',
  627. variant: variant
  628. });
  629. },
  630.  
  631. _updateHistoryState: function(variant) {
  632. if (!history.replaceState || !variant) {
  633. return;
  634. }
  635.  
  636. var newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?variant=' + variant.id;
  637. window.history.replaceState({path: newurl}, '', newurl);
  638. },
  639.  
  640. _updateMasterSelect: function(variant) {
  641. $(this.originalSelectorId, this.$container).val(variant.id);
  642. },
  643.  
  644. // _.compact from lodash
  645. // https://github.com/lodash/lodash/blob/4d4e452ade1e78c7eb890968d851f837be37e429/compact.js
  646. _compact: function(array) {
  647. var index = -1,
  648. length = array == null ? 0 : array.length,
  649. resIndex = 0,
  650. result = [];
  651.  
  652. while (++index < length) {
  653. var value = array[index];
  654. if (value) {
  655. result[resIndex++] = value;
  656. }
  657. }
  658. return result;
  659. }
  660. });
  661.  
  662. return Variants;
  663. })();
  664.  
  665. slate.rte = {
  666. init: function() {
  667. slate.rte.wrapTable();
  668. slate.rte.wrapVideo();
  669. slate.rte.imageLinks();
  670. },
  671.  
  672. wrapTable: function() {
  673. $('.rte table').wrap('<div class="table-wrapper"></div>');
  674. },
  675.  
  676. wrapVideo: function() {
  677. var $iframeVideo = $('.rte iframe[src*="youtube.com/embed"], .rte iframe[src*="player.vimeo"]');
  678. var $iframeReset = $iframeVideo.add('iframe#admin_bar_iframe');
  679.  
  680. $iframeVideo.each(function () {
  681. // Add wrapper to make video responsive
  682. if (!$(this).parents('.video-wrapper').length) {
  683. $(this).wrap('<div class="video-wrapper"></div>');
  684. }
  685. });
  686.  
  687. $iframeReset.each(function () {
  688. // Re-set the src attribute on each iframe after page load
  689. // for Chrome's "incorrect iFrame content on 'back'" bug.
  690. // https://code.google.com/p/chromium/issues/detail?id=395791
  691. // Need to specifically target video and admin bar
  692. this.src = this.src;
  693. });
  694. },
  695.  
  696. // Remove CSS that adds animated underline under image links
  697. imageLinks: function() {
  698. $('.rte a img').parent().addClass('rte__image');
  699. }
  700. };
  701.  
  702.  
  703. theme.Modals = (function() {
  704. function Modal(id, name, options) {
  705. var defaults = {
  706. close: '.js-modal-close',
  707. open: '.js-modal-open-' + name,
  708. openClass: 'modal--is-active',
  709. bodyOpenClass: 'modal-open',
  710. closeOffContentClick: true
  711. };
  712.  
  713. this.id = id;
  714. this.$modal = $('#' + id);
  715.  
  716. if (!this.$modal.length) {
  717. return false;
  718. }
  719.  
  720. this.nodes = {
  721. $parent: $('html').add('body'),
  722. $modalContent: this.$modal.find('.modal__inner')
  723. };
  724.  
  725. this.config = $.extend(defaults, options);
  726. this.modalIsOpen = false;
  727. this.$focusOnOpen = this.config.focusOnOpen ? $(this.config.focusOnOpen) : this.$modal;
  728.  
  729. this.init();
  730. }
  731.  
  732. Modal.prototype.init = function() {
  733. var $openBtn = $(this.config.open);
  734.  
  735. // Add aria controls
  736. $openBtn.attr('aria-expanded', 'false');
  737.  
  738. $(this.config.open).on('click', this.open.bind(this));
  739. this.$modal.find(this.config.close).on('click', this.close.bind(this));
  740.  
  741. // Close modal if a drawer is opened
  742. $('body').on('drawerOpen', function() {
  743. this.close();
  744. }.bind(this));
  745. };
  746.  
  747. Modal.prototype.open = function(evt) {
  748. // Keep track if modal was opened from a click, or called by another function
  749. var externalCall = false;
  750.  
  751. // don't open an opened modal
  752. if (this.modalIsOpen) {
  753. return;
  754. }
  755.  
  756. // Prevent following href if link is clicked
  757. if (evt) {
  758. evt.preventDefault();
  759. } else {
  760. externalCall = true;
  761. }
  762.  
  763. // Without this, the modal opens, the click event bubbles up to $nodes.page
  764. // which closes the modal.
  765. if (evt && evt.stopPropagation) {
  766. evt.stopPropagation();
  767. // save the source of the click, we'll focus to this on close
  768. this.$activeSource = $(evt.currentTarget);
  769. }
  770.  
  771. if (this.modalIsOpen && !externalCall) {
  772. this.close();
  773. }
  774.  
  775. this.$modal.addClass(this.config.openClass);
  776. this.nodes.$parent.addClass(this.config.bodyOpenClass);
  777.  
  778. setTimeout(function() {
  779. this.$modal.addClass('aos-animate');
  780. }.bind(this), 0);
  781.  
  782. this.modalIsOpen = true;
  783.  
  784. slate.a11y.trapFocus({
  785. $container: this.$modal,
  786. $elementToFocus: this.$focusOnOpen,
  787. namespace: 'modal_focus'
  788. });
  789.  
  790. if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
  791. this.$activeSource.attr('aria-expanded', 'true');
  792. }
  793.  
  794. $('body').trigger('modalOpen.' + this.id);
  795.  
  796. this.bindEvents();
  797. };
  798.  
  799. Modal.prototype.close = function() {
  800. // don't close a closed modal
  801. if (!this.modalIsOpen) {
  802. return;
  803. }
  804.  
  805. // deselect any focused form elements
  806. $(document.activeElement).trigger('blur');
  807.  
  808. this.$modal.removeClass(this.config.openClass).removeClass('aos-animate');
  809. this.nodes.$parent.removeClass(this.config.bodyOpenClass);
  810.  
  811. this.modalIsOpen = false;
  812.  
  813. slate.a11y.removeTrapFocus({
  814. $container: this.$modal,
  815. namespace: 'modal_focus'
  816. });
  817.  
  818. if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
  819. this.$activeSource.attr('aria-expanded', 'false').focus();
  820. }
  821.  
  822. $('body').trigger('modalClose.' + this.id);
  823.  
  824. this.unbindEvents();
  825. };
  826.  
  827. Modal.prototype.bindEvents = function() {
  828. // Pressing escape closes modal
  829. this.nodes.$parent.on('keyup.modal', function(evt) {
  830. if (evt.keyCode === 27) {
  831. this.close();
  832. }
  833. }.bind(this));
  834.  
  835. if (this.config.closeOffContentClick) {
  836. // Clicking outside of the modal content also closes it
  837. this.$modal.on('click.modal', this.close.bind(this));
  838.  
  839. // Exception to above: clicking anywhere on the modal content will NOT close it
  840. this.nodes.$modalContent.on('click.modal', function(evt) {
  841. evt.stopImmediatePropagation();
  842. });
  843. }
  844. };
  845.  
  846. Modal.prototype.unbindEvents = function() {
  847. this.nodes.$parent.off('.modal');
  848.  
  849. if (this.config.closeOffContentClick) {
  850. this.$modal.off('.modal');
  851. this.nodes.$modalContent.off('.modal');
  852. }
  853. };
  854.  
  855. return Modal;
  856. })();
  857.  
  858. theme.ProductScreen = (function() {
  859.  
  860. var originalTitle = document.title;
  861. var namespace = 'productscreen';
  862. var windowPosition = 0;
  863. var $page = $('#MainContent');
  864.  
  865. function ProductScreen(id, name, options) {
  866. var defaults = {
  867. close: '.js-screen-close',
  868. open: '.js-screen-open-' + name,
  869. openClass: 'screen-layer--is-active',
  870. closeSlideAnimate: 'screen-layer--is-sliding',
  871. bodyOpenClass: 'screen-layer-open',
  872. bodyClosingClass: 'screen-layer-closing',
  873. bodyCloseAnimate: 'screen-layer-closing screen-layer-animating',
  874. loaderStart: 200,
  875. pullToCloseThreshold: -100
  876. };
  877.  
  878. this.id = id;
  879. this.$screen = $('#' + id);
  880. this.title = this.$screen.data('product-title');
  881.  
  882. if (!this.$screen.length) {
  883. return false;
  884. }
  885.  
  886. this.nodes = {
  887. $parent: $('html').add('body'),
  888. $body: $('body'),
  889. $loader: $('#OverscrollLoader').find('.icon-loader__path'),
  890. $screenContent: this.$screen.find('.screen-layer__inner'),
  891. $photoswipe: $('.pswp')
  892. };
  893.  
  894. this.config = $.extend(defaults, options);
  895. this.initalized = false; // opened at least once
  896. this.isOpen = false;
  897. this.$focusOnOpen = this.config.focusOnOpen ? $(this.config.focusOnOpen) : this.$screen;
  898.  
  899. this.init();
  900. }
  901.  
  902. ProductScreen.prototype.init = function() {
  903. var $openBtn = $(this.config.open);
  904.  
  905. // Add aria controls
  906. $openBtn.attr('aria-expanded', 'false');
  907.  
  908. $(this.config.open).on('click', this.open.bind(this));
  909. this.$screen.find(this.config.close).on('click', { noAnimate: true, back: true }, this.close.bind(this));
  910.  
  911. // Close screen if product added to sticky cart
  912. if (theme.settings.cartType === 'sticky') {
  913. this.nodes.$body.on('added.' + this.id, function() {
  914. theme.headerNav.toggleThumbMenu(false, true);
  915. var args = { back: true };
  916. this.close(false, args);
  917. }.bind(this));
  918.  
  919. this.nodes.$body.on('error.' + this.id, function() {
  920. if (this.initalized) {
  921. this.open();
  922. }
  923. }.bind(this));
  924. }
  925. };
  926.  
  927. ProductScreen.prototype.open = function(evt, data) {
  928. // Keep track if modal was opened from a click, or called by another function
  929. var externalCall = false;
  930. var args = {
  931. updateCurrentPath: data ? data.updateCurrentPath : true
  932. };
  933.  
  934. if (this.isOpen) {
  935. return;
  936. }
  937.  
  938. // Prevent following href if link is clicked
  939. if (evt) {
  940. evt.preventDefault();
  941. } else {
  942. externalCall = true;
  943. }
  944.  
  945. // Without this, the modal opens, the click event bubbles up to $nodes.page
  946. // which closes the modal.
  947. if (evt && evt.stopPropagation) {
  948. evt.stopPropagation();
  949. // save the source of the click, we'll focus to this on close
  950. this.$activeSource = $(evt.currentTarget);
  951. }
  952.  
  953. if (this.isOpen && !externalCall) {
  954. this.close();
  955. }
  956.  
  957. windowPosition = window.scrollY;
  958.  
  959. this.$screen
  960. .prepareTransition()
  961. .addClass(this.config.openClass);
  962. this.nodes.$parent.addClass(this.config.bodyOpenClass);
  963. this.nodes.$screenContent.scrollTop(0);
  964. window.scrollTo(0,0);
  965.  
  966. slate.a11y.trapFocus({
  967. $container: this.$screen,
  968. $elementToFocus: this.$focusOnOpen,
  969. namespace: namespace
  970. });
  971.  
  972. if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
  973. this.$activeSource.attr('aria-expanded', 'true');
  974. }
  975.  
  976. var newUrl = this.$activeSource.data('url');
  977. this.nodes.$body
  978. .trigger('productModalOpen.' + this.id)
  979. .trigger('newPopstate', {screen: this, url: newUrl, updateCurrentPath: args.updateCurrentPath});
  980.  
  981. this.initalized = true;
  982. this.isOpen = true;
  983. document.title = this.title;
  984.  
  985. // Trigger Google Analytics page view if enabled
  986. if (window.ga) { ga('send', 'pageview', { page: newUrl }) }
  987.  
  988. this.bindEvents();
  989. };
  990.  
  991. ProductScreen.prototype.close = function(evt, args) {
  992. var evtData = args ? args : (evt ? evt.data : null);
  993. var goBack = evtData ? evtData.back : false;
  994. var noAnimate = (evtData && evtData.noAnimate) ? true : false;
  995. this.nodes.$body.removeAttr('style');
  996. this.nodes.$loader.css('stroke-dashoffset', this.config.loaderStart);
  997.  
  998. if (goBack) {
  999. this.nodes.$body.trigger('newPopstate', {screen: this, back: true});
  1000. }
  1001.  
  1002. var closeClass = noAnimate ? '' : this.config.closeSlideAnimate;
  1003. var bodyCloseClass = noAnimate ? this.config.bodyClosingClass : this.config.bodyCloseAnimate;
  1004.  
  1005. // Don't close if already closed
  1006. if (!this.isOpen) {
  1007. return;
  1008. }
  1009.  
  1010. // deselect any focused form elements
  1011. $(document.activeElement).trigger('blur');
  1012.  
  1013. this.$screen
  1014. .prepareTransition()
  1015. .removeClass(this.config.openClass)
  1016. .addClass(closeClass);
  1017. this.nodes.$parent
  1018. .removeClass(this.config.bodyOpenClass)
  1019. .addClass(bodyCloseClass);
  1020.  
  1021. window.setTimeout(function() {
  1022. this.$screen.removeClass(closeClass);
  1023. this.nodes.$parent.removeClass(bodyCloseClass);
  1024. window.scrollTo(0, windowPosition);
  1025. }.bind(this), 500); // duration of css animation
  1026.  
  1027. slate.a11y.removeTrapFocus({
  1028. $container: this.$screen,
  1029. namespace: namespace
  1030. });
  1031.  
  1032. if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
  1033. this.$activeSource.attr('aria-expanded', 'false').focus();
  1034. }
  1035.  
  1036. this.nodes.$body
  1037. .trigger('productModalClose')
  1038. .trigger('productModalClose.' + this.id);
  1039.  
  1040. window.scrollTo(0, windowPosition);
  1041.  
  1042. this.isOpen = false;
  1043. document.title = originalTitle;
  1044.  
  1045. if (window.ga) { ga('send', 'pageview') }
  1046.  
  1047. this.unbindEvents();
  1048. };
  1049.  
  1050. ProductScreen.prototype.bindEvents = function() {
  1051. // Pressing escape closes modal, unless the photoswipe screen is open
  1052. this.nodes.$body.on('keyup.' + namespace, function(evt) {
  1053. if (evt.keyCode === 27) {
  1054. if (this.nodes.$photoswipe.hasClass('pswp--open')) {
  1055. return;
  1056. }
  1057. if (this.nodes.$body.hasClass('js-drawer-open')) {
  1058. return;
  1059. }
  1060. var args = { back: true };
  1061. this.close(false, args);
  1062. }
  1063. }.bind(this));
  1064.  
  1065. // If scrolling up while at top, close modal
  1066. var bgAmount = 0;
  1067. var loaderAmount = 0;
  1068. $(document).on('touchmove.' + namespace, $.throttle(15, function(evt) {
  1069. var pos = window.scrollY;
  1070.  
  1071. if (pos >= 0) {
  1072. return;
  1073. }
  1074.  
  1075. bgAmount = -(pos/100);
  1076. this.nodes.$body.css('background', 'rgba(0,0,0,' + bgAmount + ')');
  1077.  
  1078. // stroke fills from 200-0 (0 = full)
  1079. loaderAmount = this.config.loaderStart + (pos * 2); // pos is negative number
  1080.  
  1081. if (pos <= this.config.pullToCloseThreshold) {
  1082. loaderAmount = 0;
  1083. }
  1084.  
  1085. this.nodes.$loader.css('stroke-dashoffset', loaderAmount);
  1086. }.bind(this)));
  1087.  
  1088. $(document).on('touchend.' + namespace, function(evt) {
  1089. totalLoader = this.config.loaderStart; // reset to starting point
  1090. var pos = window.scrollY;
  1091. if (pos < this.config.pullToCloseThreshold) {
  1092. var args = { back: true };
  1093. this.close(false, args);
  1094. }
  1095. }.bind(this));
  1096. };
  1097.  
  1098. ProductScreen.prototype.unbindEvents = function() {
  1099. this.nodes.$body.off('.' + namespace);
  1100. $(document).off('.' + namespace);
  1101. };
  1102.  
  1103. return ProductScreen;
  1104. })();
  1105.  
  1106. theme.Drawers = (function() {
  1107. function Drawer(id, name) {
  1108. this.config = {
  1109. id: id,
  1110. close: '.js-drawer-close',
  1111. open: '.js-drawer-open-' + name,
  1112. openClass: 'js-drawer-open',
  1113. closingClass: 'js-drawer-closing',
  1114. activeDrawer: 'drawer--is-open',
  1115. namespace: '.drawer-' + name
  1116. };
  1117.  
  1118. this.$nodes = {
  1119. parent: $(document.documentElement).add('body'),
  1120. page: $('body')
  1121. };
  1122.  
  1123. this.$drawer = $('#' + id);
  1124.  
  1125. if (!this.$drawer.length) {
  1126. return false;
  1127. }
  1128.  
  1129. this.isOpen = false;
  1130. this.init();
  1131. };
  1132.  
  1133. Drawer.prototype = $.extend({}, Drawer.prototype, {
  1134. init: function() {
  1135. var $openBtn = $(this.config.open);
  1136.  
  1137. // Add aria controls
  1138. $openBtn.attr('aria-expanded', 'false');
  1139.  
  1140. $openBtn.on('click', this.open.bind(this));
  1141. this.$drawer.find(this.config.close).on('click', this.close.bind(this));
  1142. },
  1143.  
  1144. open: function(evt) {
  1145. if (evt) {
  1146. evt.preventDefault();
  1147. }
  1148.  
  1149. if (this.isOpen) {
  1150. return;
  1151. }
  1152.  
  1153. // Without this the drawer opens, the click event bubbles up to $nodes.page which closes the drawer.
  1154. if (evt && evt.stopPropagation) {
  1155. evt.stopPropagation();
  1156. // save the source of the click, we'll focus to this on close
  1157. this.$activeSource = $(evt.currentTarget);
  1158. }
  1159.  
  1160. this.$drawer.prepareTransition().addClass(this.config.activeDrawer);
  1161.  
  1162. this.$nodes.parent.addClass(this.config.openClass);
  1163. this.isOpen = true;
  1164.  
  1165. slate.a11y.trapFocus({
  1166. $container: this.$drawer,
  1167. namespace: 'drawer_focus'
  1168. });
  1169.  
  1170. $('body').trigger('drawerOpen.' + this.config.id);
  1171.  
  1172. if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
  1173. this.$activeSource.attr('aria-expanded', 'true');
  1174. }
  1175.  
  1176. this.bindEvents();
  1177. },
  1178.  
  1179. close: function() {
  1180. if (!this.isOpen) {
  1181. return;
  1182. }
  1183.  
  1184. // deselect any focused form elements
  1185. $(document.activeElement).trigger('blur');
  1186.  
  1187. this.$drawer.prepareTransition().removeClass(this.config.activeDrawer);
  1188.  
  1189. this.$nodes.parent.removeClass(this.config.openClass);
  1190. this.$nodes.parent.addClass(this.config.closingClass);
  1191. var o = this;
  1192. window.setTimeout(function() {
  1193. o.$nodes.parent.removeClass(o.config.closingClass);
  1194. }, 500);
  1195.  
  1196. this.isOpen = false;
  1197.  
  1198. slate.a11y.removeTrapFocus({
  1199. $container: this.$drawer,
  1200. namespace: 'drawer_focus'
  1201. });
  1202.  
  1203. if (this.$activeSource && this.$activeSource.attr('aria-expanded')) {
  1204. this.$activeSource.attr('aria-expanded', 'false');
  1205. }
  1206.  
  1207. this.unbindEvents();
  1208. },
  1209.  
  1210. bindEvents: function() {
  1211. slate.a11y.lockMobileScrolling(this.config.namespace, this.$nodes.page);
  1212.  
  1213. // Clicking body closes drawer
  1214. this.$nodes.page.on('click' + this.config.namespace, function (evt) {
  1215. if (evt.target === this.$nodes.page[0]) {
  1216. this.close();
  1217. return false;
  1218. }
  1219. }.bind(this));
  1220.  
  1221. // Pressing escape closes drawer
  1222. this.$nodes.parent.on('keyup' + this.config.namespace, function(evt) {
  1223. if (evt.keyCode === 27) {
  1224. this.close();
  1225. }
  1226. }.bind(this));
  1227. },
  1228.  
  1229. unbindEvents: function() {
  1230. slate.a11y.unlockMobileScrolling(this.config.namespace, this.$nodes.page);
  1231. this.$nodes.parent.off(this.config.namespace);
  1232. this.$nodes.page.off(this.config.namespace);
  1233. }
  1234. });
  1235.  
  1236. return Drawer;
  1237. })();
  1238.  
  1239. theme.cart = {
  1240. getCart: function() {
  1241. return $.getJSON('/cart.js');
  1242. },
  1243.  
  1244. changeItem: function(key, qty) {
  1245. return this._updateCart({
  1246. type: 'POST',
  1247. url: '/cart/change.js',
  1248. data: 'quantity=' + qty + '&id=' + key,
  1249. dataType: 'json'
  1250. });
  1251. },
  1252.  
  1253. addItemFromForm: function(data) {
  1254. return this._updateCart({
  1255. type: 'POST',
  1256. url: '/cart/add.js',
  1257. data: data,
  1258. dataType: 'json'
  1259. });
  1260. },
  1261.  
  1262. _updateCart: function(params) {
  1263. return $.ajax(params)
  1264. .then(function(cart) {
  1265. $('body').trigger('updateCart', cart);
  1266. return cart;
  1267. }.bind(this))
  1268. },
  1269.  
  1270. updateNote: function(note) {
  1271. var params = {
  1272. type: 'POST',
  1273. url: '/cart/update.js',
  1274. data: 'note=' + theme.cart.attributeToString(note),
  1275. dataType: 'json',
  1276. success: function(cart) {},
  1277. error: function(XMLHttpRequest, textStatus) {}
  1278. };
  1279.  
  1280. $.ajax(params);
  1281. },
  1282.  
  1283. updateCurrency: function(code) {
  1284. var params = {
  1285. type: 'POST',
  1286. url: '/cart/update.js',
  1287. data: 'currency=' + code,
  1288. dataType: 'json',
  1289. success: function(cart) {
  1290. location.reload(); // required for multi-currency feature to update
  1291. },
  1292. error: function(XMLHttpRequest, textStatus) {}
  1293. };
  1294.  
  1295. $.ajax(params);
  1296. },
  1297.  
  1298. attributeToString: function(attribute) {
  1299. if ((typeof attribute) !== 'string') {
  1300. attribute += '';
  1301. if (attribute === 'undefined') {
  1302. attribute = '';
  1303. }
  1304. }
  1305. return $.trim(attribute);
  1306. }
  1307. }
  1308.  
  1309. $(function() {
  1310. // Add a loading indicator on the cart checkout button (/cart and drawer)
  1311. $('body').on('click', '.cart__checkout', function() {
  1312. $(this).addClass('btn--loading');
  1313. });
  1314.  
  1315. $('body').on('change', 'textarea[name="note"]', function() {
  1316. var newNote = $(this).val();
  1317. theme.cart.updateNote(newNote);
  1318. });
  1319.  
  1320.  
  1321. // Custom JS to prevent checkout without confirming terms and conditions
  1322. $('body').on('click', '.cart__checkout--ajax', function(evt) {
  1323. if ($('#CartAgree').is(':checked')) {
  1324. } else {
  1325. alert(theme.strings.cartTermsConfirmation);
  1326. $(this).removeClass('btn--loading');
  1327. return false;
  1328. }
  1329. });
  1330.  
  1331. $('body').on('click', '.cart__checkout--page', function(evt) {
  1332. if ($('#CartPageAgree').is(':checked')) {
  1333. } else {
  1334. alert(theme.strings.cartTermsConfirmation);
  1335. $(this).removeClass('btn--loading');
  1336. return false;
  1337. }
  1338. });
  1339. });
  1340.  
  1341. theme.AjaxCart = (function() {
  1342. var config = {
  1343. namespace: '.ajaxcart'
  1344. };
  1345.  
  1346. var data = {
  1347. itemId: 'data-cart-item-id'
  1348. };
  1349.  
  1350. var selectors = {
  1351. form: 'form.cart',
  1352. cartCount: '.cart-link__count',
  1353. updateBtn: '.update-cart',
  1354.  
  1355. itemList: '[data-cart-item-list]',
  1356. item: '[data-cart-item]',
  1357. itemId: '[data-cart-item-id]',
  1358. itemHref: '[data-cart-item-href]',
  1359. itemBackgroundImage: '[data-cart-item-background-image]',
  1360. itemTitle: '[data-cart-item-title]',
  1361. itemVariantTitle: '[data-cart-item-variant-title]',
  1362. itemPropertyList: '[data-cart-item-property-list]',
  1363. itemProperty: '[data-cart-item-property]',
  1364. itemDiscountList: '[data-cart-item-discount-list]',
  1365. itemDiscount: '[data-cart-item-discount]',
  1366. itemDiscountTitle: '[data-cart-item-discount-title]',
  1367. itemDiscountAmount: '[data-cart-item-discount-amount]',
  1368. itemLabelQuantity: '[data-cart-item-label-quantity]',
  1369. itemInputQuantity: '[data-cart-item-input-quantity]',
  1370. itemDelete: '[data-cart-item-delete]',
  1371. itemPriceContainer: '[data-cart-item-price-container]',
  1372. itemLinePriceContainer: '[data-cart-item-line-price-container]',
  1373. itemUnitPrice: '[data-cart-item-unit-price]',
  1374. itemMessage: '[data-item-message]',
  1375. cartDiscountContainer: '[data-cart-discount-container]',
  1376. cartDiscountContent: '[data-cart-discount-content]',
  1377. cartDiscount: '[data-cart-discount]',
  1378. cartDiscountTitle: '[data-cart-discount-title]',
  1379. cartDiscountAmount: '[data-cart-discount-amount]',
  1380. cartNoteContainer: '[data-cart-note-container]',
  1381. cartNoteInput: '[data-cart-note]',
  1382. cartMessage: '[data-cart-message]',
  1383. cartSubtotal: '[data-cart-subtotal]',
  1384. cartSubmit: '[data-cart-submit]',
  1385.  
  1386. fixedFooter: '.drawer__footer--fixed',
  1387. fixedInnerContent: '.drawer__inner--has-fixed-footer'
  1388. };
  1389.  
  1390. var classes = {
  1391. cartHasItems: 'cart-has-items',
  1392. cartTemplate: 'ajax-cart__template',
  1393. cartItemRemove: 'cart__item--remove',
  1394. staticDrawerElement: 'drawer--cart--static'
  1395. };
  1396.  
  1397. function AjaxCart(id) {
  1398. this.id = id;
  1399. var $container = this.$container = $('#' + id);
  1400.  
  1401. this.status = {
  1402. loaded: false,
  1403. loading: false,
  1404. isDrawer: $container.attr('data-drawer')
  1405. };
  1406.  
  1407. if (this.status.isDrawer) {
  1408. this.drawer = new theme.Drawers(id, 'cart');
  1409. }
  1410.  
  1411. this.init();
  1412. this.initEventListeners();
  1413. };
  1414.  
  1415. AjaxCart.prototype = $.extend({}, AjaxCart.prototype, {
  1416. init: function() {
  1417. this.$form = $(selectors.form, this.$container);
  1418. $(selectors.updateBtn, this.$form).addClass('hide');
  1419. this.$itemTemplate = $(selectors.item, this.$form).first().clone();
  1420. this.$propertyTemplate = $(selectors.itemProperty, this.$form).first().clone();
  1421. this.$discountTemplate = $(selectors.itemDiscount, this.$form).first().clone();
  1422. this.$cartDiscountTemplate = $(selectors.cartDiscount, this.$container).first().clone();
  1423. },
  1424.  
  1425. initEventListeners: function() {
  1426. $('body').on('updateCart', function(evt, cart) {
  1427. theme.cart.getCart().then(function(cart) {
  1428. this.buildCart(cart);
  1429. this.updateCartNotification(cart);
  1430.  
  1431. // Open cart once updated
  1432. var openDrawer = false;
  1433. if (this.status.isDrawer) {
  1434. this.drawer.open();
  1435. openDrawer = true;
  1436. }
  1437. }.bind(this));
  1438. }.bind(this));
  1439.  
  1440. this.$container.on('click', selectors.itemDelete, this._onItemDelete.bind(this));
  1441. this.$container.on('input', selectors.itemInputQuantity, $.debounce(500, this._onItemQuantityChange.bind(this)));
  1442. this.$container.on('blur', selectors.itemInputQuantity, this._onItemQuantityEmptyBlur.bind(this));
  1443. this.$container.on('focus', selectors.itemInputQuantity, this._highlightText);
  1444.  
  1445. if (this.status.isDrawer) {
  1446. $('body')
  1447. .on('updateCart' + config.namespace, this.sizeCart.bind(this))
  1448. .on('drawerOpen.' + this.id, function() {
  1449. this.sizeCart();
  1450. setTimeout(function() {
  1451. this.sizeCart();
  1452. }.bind(this), 500);
  1453. }.bind(this));
  1454.  
  1455. $(window).on('resize' + config.namespace, $.debounce(150, this.sizeCart.bind(this)));
  1456. }
  1457. },
  1458.  
  1459. buildCart: function(cart, openDrawer) {
  1460. this.loading(true);
  1461.  
  1462. this.$form.removeClass('cart--empty');
  1463.  
  1464. if (cart.item_count === 0) {
  1465. this.$form.addClass('cart--empty');
  1466. this.status.loaded = true;
  1467. this.loading(false);
  1468. return;
  1469. }
  1470.  
  1471. // If 3+ items, remove static class for a 100% height cart drawer
  1472. if (cart.items.length > 2) {
  1473. this.$container.removeClass(classes.staticDrawerElement);
  1474. } else {
  1475. this.$container.addClass(classes.staticDrawerElement);
  1476. }
  1477.  
  1478. var $cart = this._createCart(cart);
  1479. morphdom(this.$form[0], $cart[0]);
  1480.  
  1481. if (theme.settings.currenciesEnabled) {
  1482. theme.currencySwitcher.ajaxrefresh();
  1483. }
  1484.  
  1485. if (window.Shopify && Shopify.StorefrontExpressButtons) {
  1486. Shopify.StorefrontExpressButtons.initialize();
  1487.  
  1488. // Resize footer after arbitrary delay to account for checkout buttons
  1489. if (this.status.isDrawer) {
  1490. setTimeout(function() {
  1491. this.sizeCart();
  1492. }.bind(this), 1000);
  1493. }
  1494. }
  1495.  
  1496. // If specifically asked, open the cart drawer (only happens after product added from form)
  1497. if (this.status.isDrawer) {
  1498. if (openDrawer === true) {
  1499. this.drawer.open();
  1500. }
  1501. }
  1502.  
  1503. this.status.loaded = true;
  1504. this.loading(false);
  1505.  
  1506. document.dispatchEvent(new CustomEvent('cart:updated', {
  1507. detail: {
  1508. cart: cart
  1509. }
  1510. }));
  1511. },
  1512.  
  1513. _createCart: function(cart) {
  1514. var $form = this.$form.clone();
  1515.  
  1516. $(selectors.item, $form)
  1517. .not(selectors.cartNoteContainer)
  1518. .remove();
  1519.  
  1520. $(selectors.itemList, $form)
  1521. .prepend(this._createItemList(cart));
  1522.  
  1523. $(selectors.cartNoteInput, $form)
  1524. .val(cart.note);
  1525.  
  1526. $(selectors.cartDiscountContainer, $form)
  1527. .toggleClass('hide', cart.cart_level_discount_applications.length === 0);
  1528.  
  1529. $(selectors.cartDiscountContent, $form).html(
  1530. this._createCartDiscountList(cart));
  1531.  
  1532. $(selectors.cartSubtotal, $form)
  1533. .html(theme.Currency.formatMoney(cart.total_price, theme.settings.moneyFormat));
  1534.  
  1535. $(selectors.cartSubmit, $form).attr('disabled', cart.items.length === 0);
  1536.  
  1537. return $form;
  1538. },
  1539.  
  1540. _createItemList: function(cart) {
  1541. return $.map(cart.items, function(item) {
  1542. var $item = this.$itemTemplate.clone().removeClass(classes.cartTemplate);
  1543. var propertyList = this._createPropertyList(item);
  1544. var discountList = this._createDiscountList(item);
  1545. var unitPrice = this._createUnitPrice(item);
  1546.  
  1547. var itemPrice = this._createItemPrice(
  1548. item.original_price,
  1549. item.final_price
  1550. );
  1551.  
  1552. var itemLinePrice = this._createItemPrice(
  1553. item.original_line_price,
  1554. item.final_line_price
  1555. );
  1556.  
  1557. $item.find(selectors.itemId)
  1558. .addBack(selectors.itemId)
  1559. .attr(data.itemId, item.key);
  1560.  
  1561. $(selectors.itemHref, $item)
  1562. .attr('href', item.url);
  1563.  
  1564. $(selectors.itemBackgroundImage, $item)
  1565. .removeAttr('data-bgset')
  1566. .css('background-image', item.image ? 'url(' + theme.Images.getSizedImageUrl(item.image, '200x') + ')' : 'none')
  1567. .addClass('lazyload');
  1568.  
  1569. $(selectors.itemTitle, $item).text(item.product_title);
  1570.  
  1571. $(selectors.itemVariantTitle, $item).text(item.variant_title);
  1572.  
  1573. $(selectors.itemPriceContainer, $item).html(itemPrice);
  1574.  
  1575. $(selectors.itemLinePriceContainer, $item).html(itemLinePrice);
  1576.  
  1577. $(selectors.itemLinePrice, $item)
  1578. .html(theme.Currency.formatMoney(item.line_price, theme.settings.moneyFormat));
  1579.  
  1580. $(selectors.itemLabelQuantity, $item)
  1581. .attr('for', 'quantity_' + item.key);
  1582.  
  1583. $(selectors.itemInputQuantity, $item)
  1584. .attr('name', 'updates[' + item.key + ']')
  1585. .attr('id', 'quantity_' + item.key)
  1586. .val(item.quantity);
  1587.  
  1588. $(selectors.itemPropertyList, $item)
  1589. .html(propertyList);
  1590.  
  1591. $(selectors.itemDiscountList, $item)
  1592. .html(discountList);
  1593.  
  1594. $(selectors.itemUnitPrice, $item)
  1595. .html(unitPrice);
  1596.  
  1597. return $item[0];
  1598. }.bind(this));
  1599. },
  1600.  
  1601. _createItemPrice: function(original_price, final_price) {
  1602. if (original_price !== final_price) {
  1603. return (
  1604. '<span class="visually-hidden">' + theme.strings.regularPrice + '</span>' +
  1605. '<del class="cart__item-price--original">' +
  1606. theme.Currency.formatMoney(original_price, theme.settings.moneyFormat) +
  1607. '</del>' +
  1608. '<span class="visually-hidden">' + theme.strings.salePrice + '</span>' +
  1609. '<span class="cart__item-price cart__item-price--bold">' +
  1610. theme.Currency.formatMoney(final_price, theme.settings.moneyFormat) +
  1611. '</span>'
  1612. );
  1613. } else {
  1614. return (
  1615. '<span class="cart__item-price">' + theme.Currency.formatMoney(original_price, theme.settings.moneyFormat) + '</span>'
  1616. );
  1617. }
  1618. },
  1619.  
  1620. _createPropertyList: function(item) {
  1621. return $.map(item.properties, function(value, key) {
  1622. var $property = this.$propertyTemplate.clone().removeClass(classes.cartTemplate);
  1623.  
  1624. // Line item properties prefixed with an underscore are not to be displayed
  1625. if (key.charAt(0) === '_') return;
  1626.  
  1627. // Line item properties with no value are not to be displayed
  1628. if (value === '') return;
  1629.  
  1630. if (value.indexOf('/uploads/') === -1) {
  1631. $property
  1632. .text(key + ': ' + value);
  1633. } else {
  1634. $property
  1635. .html(key + ': <a href="' + value + '">' + value.split('/').pop() + '</a>');
  1636. }
  1637.  
  1638. return $property[0];
  1639. }.bind(this));
  1640. },
  1641.  
  1642. _createDiscountList: function(item) {
  1643. return $.map(item.line_level_discount_allocations, function(discount) {
  1644. var $discount = this.$discountTemplate.clone().removeClass(classes.cartTemplate);
  1645.  
  1646. $discount
  1647. .find(selectors.itemDiscountTitle)
  1648. .text(discount.discount_application.title);
  1649. $discount
  1650. .find(selectors.itemDiscountAmount)
  1651. .html(theme.Currency.formatMoney(discount.amount, theme.settings.moneyFormat));
  1652.  
  1653. return $discount[0];
  1654. }.bind(this));
  1655. },
  1656.  
  1657. _createCartDiscountList: function(cart) {
  1658. return $.map(
  1659. cart.cart_level_discount_applications,
  1660. function(discount) {
  1661. var $discount = this.$cartDiscountTemplate.clone().removeClass(classes.cartTemplate);
  1662.  
  1663. $discount.find(selectors.cartDiscountTitle)
  1664. .text(discount.title);
  1665. $discount
  1666. .find(selectors.cartDiscountAmount)
  1667. .html(theme.Currency.formatMoney(discount.total_allocated_amount, theme.settings.moneyFormat));
  1668.  
  1669. return $discount[0];
  1670. }.bind(this)
  1671. );
  1672. },
  1673.  
  1674. _createUnitPrice: function(item) {
  1675. var price = theme.Currency.formatMoney(item.unit_price, theme.settings.moneyFormat);
  1676. var base = theme.Currency.getBaseUnit(item);
  1677.  
  1678. return price + '/' + base;
  1679. },
  1680.  
  1681. _onItemQuantityChange: function(evt) {
  1682. this.loading(true);
  1683.  
  1684. var $input = $(evt.target);
  1685. var id = $input.closest(selectors.item).attr(data.itemId);
  1686. var quantity = $input.val();
  1687.  
  1688. // Don't update the cart when a input is empty. Also make sure an input
  1689. // does not remain empty by checking blur event
  1690. if (quantity === '') { return; }
  1691.  
  1692. if (quantity == 0) {
  1693. var response = confirm(theme.strings.cartConfirmDelete);
  1694. if (response === false) {
  1695. $input.val(1);
  1696. this.loading(false);
  1697. return;
  1698. }
  1699. }
  1700.  
  1701. theme.cart.changeItem(id, quantity);
  1702. },
  1703.  
  1704. _onItemQuantityEmptyBlur: function(evt) {
  1705. var $input = $(evt.target);
  1706. var id = $input.closest(selectors.item).attr(data.itemId);
  1707. var value = $input.val();
  1708.  
  1709. if (value !== '') { return; }
  1710.  
  1711. theme.cart.getCart().then(function(cart) {
  1712. this.buildCart(cart);
  1713. }.bind(this));
  1714. },
  1715.  
  1716. _onItemDelete: function(evt) {
  1717. evt.preventDefault();
  1718.  
  1719. var $deleteButton = $(evt.target);
  1720. var $items = $(selectors.item, this.$container);
  1721. var $item = $deleteButton.closest(selectors.item);
  1722. var $note = $(selectors.cartNoteContainer, this.$container);
  1723. var id = $item.attr(data.itemId);
  1724.  
  1725. if ($items.length === 2 && $items.last().is($note)) {
  1726. $note.addClass(classes.cartItemRemove);
  1727.  
  1728. slate.a11y.promiseTransitionEnd($(selectors.itemList, this.$container)).then(function() {
  1729. $note.removeClass(classes.cartItemRemove);
  1730. });
  1731. }
  1732.  
  1733. $item.addClass(classes.cartItemRemove);
  1734.  
  1735. slate.a11y.promiseAnimationEnd($item).then(function() {
  1736. theme.cart.changeItem(id, 0);
  1737. }.bind(this));
  1738. },
  1739.  
  1740. loading: function(state) {
  1741. this.status.loading = state;
  1742.  
  1743. if (state) {
  1744. $(selectors.itemList, this.$form).addClass('loading');
  1745. } else {
  1746. $(selectors.itemList, this.$form).removeClass('loading');
  1747. }
  1748. },
  1749.  
  1750. sizeCart: function() {
  1751. // If drawer cart is set to static, measure to make sure it should
  1752. // stay that way
  1753. if (this.$container.hasClass(classes.staticDrawerElement)) {
  1754. // Add static class, measure, and decide if we need to add it back
  1755. this.$container.addClass(classes.staticDrawerElement);
  1756.  
  1757. // If cart is taller than window, remove static class so
  1758. // elements become absolute and items scrollable
  1759. if (this.$container.height() >= window.innerHeight) {
  1760. this.$container.removeClass(classes.staticDrawerElement);
  1761. }
  1762. }
  1763.  
  1764. // Stop if our drawer doesn't have a fixed footer
  1765. if (!this.$container.hasClass('drawer--has-fixed-footer')) {
  1766. return;
  1767. }
  1768.  
  1769. // Elements are reprinted regularly so selectors are not cached
  1770. var $cartFooter = this.$container.find(selectors.fixedFooter).removeAttr('style');
  1771. var $cartInner = this.$container.find(selectors.fixedInnerContent).removeAttr('style');
  1772. var cartFooterHeight = $cartFooter.outerHeight();
  1773.  
  1774. $cartInner.css('bottom', cartFooterHeight);
  1775. $cartFooter.css('height', cartFooterHeight);
  1776. },
  1777.  
  1778. updateCartNotification: function(cart) {
  1779. $(selectors.cartCount).text(cart.item_count);
  1780. $('body').toggleClass(classes.cartHasItems, cart.item_count > 0);
  1781. },
  1782.  
  1783. _highlightText: function(evt) {
  1784. // Don't want the mobile tooltip to pop up
  1785. if (!theme.config.isTouch) {
  1786. $(evt.target).select();
  1787. }
  1788. }
  1789. });
  1790.  
  1791. return AjaxCart;
  1792. })();
  1793.  
  1794. theme.StickyCart = (function() {
  1795. var config = {
  1796. namespace: '.ajaxcart'
  1797. };
  1798.  
  1799. var selectors = {
  1800. cart: '#StickyCart',
  1801. items: '#StickyItems',
  1802. subtotal: '#StickySubtotal',
  1803. submit: '#StickySubmit'
  1804. };
  1805.  
  1806. var classes = {
  1807. cartTemplate: 'template-cart',
  1808. active: 'sticky-cart--open',
  1809. activeBodyClass: 'body--sticky-cart-open'
  1810. };
  1811.  
  1812. function StickyCart() {
  1813. this.status = {
  1814. loaded: false,
  1815. loading: false,
  1816. open: $('body').hasClass(classes.activeBodyClass)
  1817. };
  1818.  
  1819. this.initEventListeners();
  1820. };
  1821.  
  1822. function refresh(cart) {
  1823. if ($('body').hasClass(classes.cartTemplate)) {
  1824. return;
  1825. }
  1826.  
  1827. if (cart.item_count > 0) {
  1828. $('body').addClass(classes.activeBodyClass);
  1829. $(selectors.cart).addClass(classes.active);
  1830. } else {
  1831. $('body').removeClass(classes.activeBodyClass);
  1832. $(selectors.cart).removeClass(classes.active);
  1833. }
  1834.  
  1835. $(selectors.items).text(theme.strings.cartItems.replace('[count]', cart.item_count));
  1836. $(selectors.subtotal).html(theme.Currency.formatMoney(cart.total_price, theme.settings.moneyFormat));
  1837.  
  1838. if (theme.settings.currenciesEnabled) {
  1839. theme.currencySwitcher.ajaxrefresh();
  1840. }
  1841. };
  1842.  
  1843. StickyCart.prototype = $.extend({}, StickyCart.prototype, {
  1844. initEventListeners: function() {
  1845. $(selectors.submit).on('click', function() {
  1846. $(this).addClass('btn--loading');
  1847. });
  1848.  
  1849. $('body').on('added.ajaxProduct', function() {
  1850. this.hideCart();
  1851. theme.cart.getCart().then(function(cart) {
  1852. this.buildCart(cart, true);
  1853. }.bind(this));
  1854. }.bind(this));
  1855. },
  1856.  
  1857. hideCart: function() {
  1858. $('body').removeClass(classes.activeBodyClass);
  1859. $(selectors.cart).removeClass(classes.active);
  1860. },
  1861.  
  1862. showCart: function(count, subtotal) {
  1863. if (count) {
  1864. $(selectors.items).text(theme.strings.cartItems.replace('[count]', count));
  1865. }
  1866. if (subtotal) {
  1867. $(selectors.subtotal).html(theme.Currency.formatMoney(subtotal, theme.settings.moneyFormat));
  1868. }
  1869.  
  1870. $('body').addClass(classes.activeBodyClass);
  1871. $(selectors.cart).addClass(classes.active);
  1872.  
  1873. this.status.open = true;
  1874.  
  1875. if (theme.settings.currenciesEnabled) {
  1876. theme.currencySwitcher.ajaxrefresh();
  1877. }
  1878. },
  1879.  
  1880. buildCart: function(cart, open) {
  1881. this.loading(true);
  1882.  
  1883. this.status.loaded = true;
  1884. this.loading(false);
  1885.  
  1886. // If specifically asked, open the cart (only happens after product added from form)
  1887. if (open === true) {
  1888. this.showCart(cart.item_count, cart.total_price);
  1889. }
  1890. },
  1891.  
  1892. loading: function(state) {
  1893. this.status.loading = state;
  1894.  
  1895. if (state) {
  1896. $(selectors.cart).addClass('is-loading');
  1897. } else {
  1898. $(selectors.cart).removeClass('is-loading');
  1899. }
  1900. },
  1901.  
  1902. updateError: function(XMLHttpRequest) {
  1903. if (XMLHttpRequest.responseJSON && XMLHttpRequest.responseJSON.description) {
  1904. console.warn(XMLHttpRequest.responseJSON.description);
  1905. }
  1906. }
  1907. });
  1908.  
  1909. return {
  1910. init: StickyCart,
  1911. refresh: refresh
  1912. }
  1913. })();
  1914.  
  1915. theme.AjaxProduct = (function() {
  1916. var status = {
  1917. loading: false
  1918. };
  1919.  
  1920. function ProductForm($form) {
  1921. this.$form = $form;
  1922. this.$addToCart = this.$form.find('.add-to-cart');
  1923. this.productId = $form.find('[name="data-product-id"]').val();
  1924.  
  1925. if (this.$form.length) {
  1926. this.$form.on('submit', this.addItemFromForm.bind(this));
  1927. }
  1928. };
  1929.  
  1930. ProductForm.prototype = $.extend({}, ProductForm.prototype, {
  1931. addItemFromForm: function(evt, callback){
  1932. evt.preventDefault();
  1933.  
  1934. if (status.loading) {
  1935. return;
  1936. }
  1937.  
  1938. this.$form.find('[data-add-to-cart]').addClass('btn--loading');
  1939.  
  1940. status.loading = true;
  1941.  
  1942. var data = this.$form.serialize();
  1943.  
  1944. $('body').trigger('added.ProductScreen-' + this.productId);
  1945.  
  1946. theme.cart.addItemFromForm(data)
  1947. .then(function(product) {
  1948. this.success(product);
  1949. }.bind(this))
  1950. .catch(function(XMLHttpRequest) {
  1951. this.error(XMLHttpRequest)
  1952. }.bind(this))
  1953. .always(function() {
  1954. status.loading = false;
  1955. this.$form.find('[data-add-to-cart]').removeClass('btn--loading');
  1956. }.bind(this));
  1957. },
  1958.  
  1959. success: function(product) {
  1960. this.$form.find('.errors').remove();
  1961. $('body').trigger('added.ajaxProduct');
  1962. document.dispatchEvent(new CustomEvent('added:ajaxProduct', {
  1963. detail: {
  1964. product: product
  1965. }
  1966. }));
  1967. },
  1968.  
  1969. error: function(XMLHttpRequest) {
  1970. this.$form.find('.errors').remove();
  1971.  
  1972. if (XMLHttpRequest.responseJSON && XMLHttpRequest.responseJSON.description) {
  1973. console.warn(XMLHttpRequest.responseJSON.description);
  1974.  
  1975. $('body').trigger('error.ProductScreen-' + this.productId);
  1976.  
  1977. document.dispatchEvent(new CustomEvent('error:ajaxProduct', {
  1978. detail: {
  1979. errorMessage: XMLHttpRequest.responseJSON.description
  1980. }
  1981. }));
  1982.  
  1983. this.$form.prepend('<div class="errors text-center">' + XMLHttpRequest.responseJSON.description + '</div>');
  1984. }
  1985. }
  1986. });
  1987.  
  1988. return ProductForm;
  1989. })();
  1990.  
  1991. // Either collapsible containers all acting individually,
  1992. // or tabs that can only have one open at a time
  1993. theme.collapsibles = (function() {
  1994.  
  1995. var selectors = {
  1996. trigger: '.collapsible-trigger',
  1997. module: '.collapsible-content',
  1998. moduleInner: '.collapsible-content__inner',
  1999. tabs: '.collapsible-trigger--tab'
  2000. };
  2001.  
  2002. var classes = {
  2003. hide: 'hide',
  2004. open: 'is-open',
  2005. autoHeight: 'collapsible--auto-height',
  2006. tabs: 'collapsible-trigger--tab'
  2007. };
  2008.  
  2009. var namespace = '.collapsible';
  2010.  
  2011. var isTransitioning = false;
  2012.  
  2013. function init() {
  2014. $(selectors.trigger).each(function() {
  2015. var $el = $(this);
  2016. var state = $el.hasClass(classes.open);
  2017. $el.attr('aria-expanded', state);
  2018. });
  2019.  
  2020. $('body')
  2021. .off(namespace)
  2022. .on('click' + namespace, selectors.trigger, function() {
  2023. if (isTransitioning) {
  2024. return;
  2025. }
  2026.  
  2027. isTransitioning = true;
  2028.  
  2029. var $el = $(this);
  2030. var isOpen = $el.hasClass(classes.open);
  2031. var isTab = $el.hasClass(classes.tabs);
  2032. var moduleId = $el.attr('aria-controls');
  2033. var $module = $('#' + moduleId);
  2034. var height = $module.find(selectors.moduleInner).outerHeight();
  2035. var isAutoHeight = $el.hasClass(classes.autoHeight);
  2036.  
  2037. if (isTab) {
  2038. if (isOpen) {
  2039. isTransitioning = false;
  2040. return;
  2041. }
  2042.  
  2043. var $newModule;
  2044. // If tab, close all other tabs with same ID before opening
  2045. $(selectors.tabs + '[data-id=' + $el.data('id') + ']').each(function() {
  2046. $(this).removeClass(classes.open);
  2047. $newModule = $('#' + $(this).attr('aria-controls'));
  2048. setTransitionHeight($newModule, 0, true);
  2049. });
  2050. }
  2051.  
  2052. // If isAutoHeight, set the height to 0 just after setting the actual height
  2053. // so the closing animation works nicely
  2054. if (isOpen && isAutoHeight) {
  2055. setTimeout(function() {
  2056. height = 0;
  2057. setTransitionHeight($module, height, isOpen, isAutoHeight);
  2058. }, 0);
  2059. }
  2060.  
  2061. if (isOpen && !isAutoHeight) {
  2062. height = 0;
  2063. }
  2064.  
  2065. $el
  2066. .attr('aria-expanded', !isOpen)
  2067. .toggleClass(classes.open, !isOpen);
  2068.  
  2069. setTransitionHeight($module, height, isOpen, isAutoHeight);
  2070. });
  2071. }
  2072.  
  2073. function setTransitionHeight($module, height, isOpen, isAutoHeight) {
  2074. $module
  2075. .removeClass(classes.hide)
  2076. .prepareTransition()
  2077. .css('height', height)
  2078. .toggleClass(classes.open, !isOpen);
  2079.  
  2080. if (!isOpen && isAutoHeight) {
  2081. var o = $module;
  2082. window.setTimeout(function() {
  2083. o.css('height','auto');
  2084. isTransitioning = false;
  2085. }, 0);
  2086. } else {
  2087. isTransitioning = false;
  2088. }
  2089. }
  2090.  
  2091. return {
  2092. init: init
  2093. };
  2094. })();
  2095.  
  2096. theme.headerNav = (function() {
  2097.  
  2098. var selectors = {
  2099. wrapper: '.header-wrapper',
  2100. siteHeader: '.site-header',
  2101. logoContainer: '.site-header__logo',
  2102. logo: '.site-header__logo img',
  2103. navigation: '.site-navigation',
  2104. navContainerWithLogo: '.header-item--logo',
  2105. navItems: '.site-nav__item',
  2106. navLinks: '.site-nav__link',
  2107. navLinksWithDropdown: '.site-nav__link--has-dropdown',
  2108. navDropdownLinks: '.site-nav__dropdown-link--second-level',
  2109. thumbMenu: '.site-nav__thumb-menu'
  2110. };
  2111.  
  2112. var classes = {
  2113. hasDropdownClass: 'site-nav--has-dropdown',
  2114. hasSubDropdownClass: 'site-nav__deep-dropdown-trigger',
  2115. dropdownActive: 'is-focused',
  2116. stickyCartActive: 'body--sticky-cart-open',
  2117. overlayEnabledClass: 'header-wrapper--overlay',
  2118. overlayedClass: 'is-light',
  2119. thumbMenuInactive: 'site-nav__thumb-menu--inactive',
  2120. stickyClass: 'site-header--sticky',
  2121. overlayStickyClass: 'header-wrapper--sticky',
  2122. openTransitionClass: 'site-header--opening'
  2123. };
  2124.  
  2125. var config = {
  2126. namespace: '.siteNav',
  2127. overlayHeader: false,
  2128. stickyActive: false,
  2129. forceStickyOnMobile: false,
  2130. forceCloseThumbNav: false
  2131. };
  2132.  
  2133. // Elements used in resize functions, defined in init
  2134. var $window;
  2135. var $navContainerWithLogo;
  2136. var $logoContainer;
  2137. var $nav;
  2138. var $wrapper;
  2139. var $siteHeader;
  2140.  
  2141. function init() {
  2142. $window = $(window);
  2143. $navContainerWithLogo = $(selectors.navContainerWithLogo);
  2144. $logoContainer = $(selectors.logoContainer);
  2145. $nav = $(selectors.navigation);
  2146. $wrapper = $(selectors.wrapper);
  2147. $siteHeader = $(selectors.siteHeader);
  2148.  
  2149. config.overlayHeader = theme.settings.overlayHeader = $siteHeader.data('overlay');
  2150.  
  2151. accessibleDropdowns();
  2152. var searchModal = new theme.Modals('SearchModal', 'search-modal', {
  2153. closeOffContentClick: false,
  2154. focusOnOpen: '#SearchModalInput'
  2155. });
  2156.  
  2157. // One listener for all header-related resize and load functions
  2158. $window
  2159. .on('resize' + config.namespace, $.debounce(150, headerResize))
  2160. .on('load' + config.namespace, headerLoad);
  2161.  
  2162. // Determine type of header:
  2163. // desktop: sticky bar | sticky button | top only
  2164. // mobile: always sticky button
  2165. setHeaderStyle();
  2166.  
  2167. // Sticky menu (bar or thumb) on scroll
  2168. $window.on('scroll' + config.namespace, $.throttle(150, stickyMenuOnScroll));
  2169.  
  2170. // Make sure sticky nav appears after header is reloaded in editor
  2171. if (Shopify.designMode) {
  2172. $window.trigger('resize');
  2173. }
  2174. }
  2175.  
  2176. function headerLoad() {
  2177. resizeLogo();
  2178. initStickyThumbMenu();
  2179.  
  2180. if (config.headerStyle === 'bar') {
  2181. initStickyBarMenu();
  2182. }
  2183. }
  2184.  
  2185. function headerResize() {
  2186. resizeLogo();
  2187. setHeaderStyle();
  2188.  
  2189. if (config.headerStyle === 'bar') {
  2190. initStickyBarMenu();
  2191. }
  2192. }
  2193.  
  2194. function setHeaderStyle() {
  2195. if (theme.config.bpSmall) {
  2196. config.headerStyle = 'button';
  2197. } else {
  2198. config.headerStyle = $wrapper.data('header-style');
  2199. }
  2200.  
  2201. config.stickyThreshold = config.headerStyle === 'button' ? 100 : 250;
  2202.  
  2203. if (config.headerStyle !== 'button') {
  2204. toggleThumbMenu(false);
  2205. }
  2206. }
  2207.  
  2208. function unload() {
  2209. $(window).off(config.namespace);
  2210. $(selectors.navLinks).off(config.namespace);
  2211. $(selectors.navDropdownLinks).off(config.namespace);
  2212. }
  2213.  
  2214. function resizeLogo() {
  2215. // Using .each() because of possible reversed color logo
  2216. $(selectors.logo).each(function() {
  2217. var $el = $(this),
  2218. logoWidthOnScreen = $el.width(),
  2219. containerWidth = $el.closest('.grid__item').width();
  2220. // If image exceeds container, let's make it smaller
  2221. if (logoWidthOnScreen > containerWidth) {
  2222. $el.css('maxWidth', containerWidth);
  2223. }
  2224. else {
  2225. $el.removeAttr('style');
  2226. }
  2227. });
  2228. }
  2229.  
  2230. function accessibleDropdowns() {
  2231. var hasActiveDropdown = false;
  2232. var hasActiveSubDropdown = false;
  2233. var closeOnClickActive = false;
  2234.  
  2235. // Touch devices open dropdown on first click, navigate to link on second
  2236. if (theme.config.isTouch) {
  2237. $(selectors.navLinksWithDropdown).on('touchend' + config.namespace, function(evt) {
  2238. var $el = $(this);
  2239. var $parentItem = $el.parent();
  2240. if (!$parentItem.hasClass(classes.dropdownActive)) {
  2241. evt.preventDefault();
  2242. closeDropdowns();
  2243. openFirstLevelDropdown($el);
  2244. } else {
  2245. window.location.replace($el.attr('href'));
  2246. }
  2247. });
  2248.  
  2249. $(selectors.navDropdownLinks).on('touchend' + config.namespace, function(evt) {
  2250. var $el = $(this);
  2251. var $parentItem = $el.parent();
  2252.  
  2253. // Open third level menu or go to link based on active state
  2254. if ($parentItem.hasClass(classes.hasSubDropdownClass)) {
  2255. if (!$parentItem.hasClass(classes.dropdownActive)) {
  2256. evt.preventDefault();
  2257. closeThirdLevelDropdown();
  2258. openSecondLevelDropdown($el);
  2259. } else {
  2260. window.location.replace($el.attr('href'));
  2261. }
  2262. } else {
  2263. // No third level nav, go to link
  2264. window.location.replace($el.attr('href'));
  2265. }
  2266. });
  2267. }
  2268.  
  2269. // Open/hide top level dropdowns
  2270. $(selectors.navLinks).on('focusin mouseover' + config.namespace, function() {
  2271. if (hasActiveDropdown) {
  2272. closeSecondLevelDropdown();
  2273. }
  2274.  
  2275. if (hasActiveSubDropdown) {
  2276. closeThirdLevelDropdown();
  2277. }
  2278.  
  2279. openFirstLevelDropdown($(this));
  2280. });
  2281.  
  2282. // Force remove focus on sitenav links because focus sometimes gets stuck
  2283. $(selectors.navLinks).on('mouseleave' + config.namespace, function() {
  2284. closeDropdowns();
  2285. });
  2286.  
  2287. // Open/hide sub level dropdowns
  2288. $(selectors.navDropdownLinks).on('focusin' + config.namespace, function() {
  2289. closeThirdLevelDropdown();
  2290. openSecondLevelDropdown($(this), true);
  2291. });
  2292.  
  2293. // Private dropdown methods
  2294. function openFirstLevelDropdown($el) {
  2295. var $parentItem = $el.parent();
  2296. if ($parentItem.hasClass(classes.hasDropdownClass)) {
  2297. $parentItem.addClass(classes.dropdownActive);
  2298. hasActiveDropdown = true;
  2299. }
  2300.  
  2301. if (!theme.config.isTouch) {
  2302. if (!closeOnClickActive) {
  2303. var eventType = theme.config.isTouch ? 'touchend' : 'click';
  2304. closeOnClickActive = true;
  2305. $('body').on(eventType + config.namespace, function() {
  2306. closeDropdowns();
  2307. $('body').off(config.namespace);
  2308. closeOnClickActive = false;
  2309. });
  2310. }
  2311. }
  2312. }
  2313.  
  2314. function openSecondLevelDropdown($el, skipCheck) {
  2315. var $parentItem = $el.parent();
  2316. if ($parentItem.hasClass(classes.hasSubDropdownClass) || skipCheck) {
  2317. $parentItem.addClass(classes.dropdownActive);
  2318. hasActiveSubDropdown = true;
  2319. }
  2320. }
  2321.  
  2322. function closeDropdowns() {
  2323. closeSecondLevelDropdown();
  2324. closeThirdLevelDropdown();
  2325. }
  2326.  
  2327. function closeSecondLevelDropdown() {
  2328. $(selectors.navItems).removeClass(classes.dropdownActive);
  2329. }
  2330.  
  2331. function closeThirdLevelDropdown() {
  2332. $(selectors.navDropdownLinks).parent().removeClass(classes.dropdownActive);
  2333. }
  2334. }
  2335.  
  2336. function initStickyBarMenu() {
  2337. $siteHeader.wrap('<div class="site-header-sticky"></div>');
  2338.  
  2339. // No need to set a height on wrapper if positioned absolutely already
  2340. if (config.overlayHeader) {
  2341. return;
  2342. }
  2343.  
  2344. stickyHeaderHeight();
  2345. setTimeout(function() {
  2346. stickyHeaderHeight();
  2347.  
  2348. // Don't let height get stuck on 0
  2349. if ($('.site-header-sticky').outerHeight() === 0) {
  2350. setTimeout(function() {
  2351. $window.trigger('resize');
  2352. }, 500);
  2353. }
  2354. }, 200);
  2355.  
  2356. $window.on('resize' + config.namespace, $.debounce(50, stickyHeaderHeight));
  2357. }
  2358.  
  2359. function stickyHeaderHeight() {
  2360. $('.site-header-sticky').css('height', $siteHeader.outerHeight(true));
  2361. }
  2362.  
  2363. function initStickyThumbMenu() {
  2364. if ($('body').hasClass(classes.stickyCartActive)) {
  2365. return;
  2366. }
  2367.  
  2368. if (theme.config.bpSmall && theme.template !== 'product') {
  2369. setTimeout(function() {
  2370. config.forceStickyOnMobile = true;
  2371. toggleThumbMenu(true);
  2372. }, 25);
  2373. }
  2374. }
  2375.  
  2376. function stickyMenuOnScroll(evt) {
  2377. var scroll = $window.scrollTop();
  2378.  
  2379. if (scroll > config.stickyThreshold) {
  2380. if (config.forceStickyOnMobile) {
  2381. config.forceStickyOnMobile = false;
  2382. }
  2383.  
  2384. if (config.stickyActive) {
  2385. return;
  2386. }
  2387.  
  2388. if (config.headerStyle === 'button') {
  2389. toggleThumbMenu(true);
  2390. } else if (config.headerStyle === 'bar') {
  2391. toggleBarMenu(true);
  2392. }
  2393. } else {
  2394. // If menu is shown on mobile page load, do not
  2395. // automatically hide it when you start scrolling
  2396. if (config.forceStickyOnMobile) {
  2397. return;
  2398. }
  2399.  
  2400. if (!config.stickyActive) {
  2401. return;
  2402. }
  2403.  
  2404. if (config.headerStyle === 'button') {
  2405. if (!theme.config.bpSmall) {
  2406. toggleThumbMenu(false);
  2407. }
  2408. } else if (config.headerStyle === 'bar') {
  2409. toggleBarMenu(false);
  2410. }
  2411. }
  2412. }
  2413.  
  2414. function toggleThumbMenu(active, forceClose) {
  2415. // If forced close, will not open again until page refreshes
  2416. // because sticky nav is open
  2417. if (config.forceCloseThumbNav) {
  2418. return;
  2419. }
  2420.  
  2421. // If thumb menu is open, do not hide menu button
  2422. if ($('.slide-nav__overflow--thumb').hasClass('js-menu--is-open')) {
  2423. return;
  2424. }
  2425.  
  2426. $(selectors.thumbMenu).toggleClass(classes.thumbMenuInactive, !active);
  2427. config.stickyActive = active;
  2428.  
  2429. config.forceCloseThumbNav = forceClose;
  2430. }
  2431.  
  2432. function toggleBarMenu(active) {
  2433. if (config.headerStyle !== 'bar') {
  2434. return;
  2435. }
  2436.  
  2437. if (active) {
  2438. $siteHeader.addClass(classes.stickyClass);
  2439. if (config.overlayHeader) {
  2440. $wrapper
  2441. .removeClass(classes.overlayedClass)
  2442. .addClass(classes.overlayStickyClass);
  2443. }
  2444.  
  2445. // Add open transition class after element is set to fixed
  2446. // so CSS animation is applied correctly
  2447. setTimeout(function() {
  2448. $siteHeader.addClass(classes.openTransitionClass);
  2449. }, 100);
  2450. } else {
  2451. $siteHeader.removeClass(classes.openTransitionClass).removeClass(classes.stickyClass);
  2452.  
  2453. if (config.overlayHeader) {
  2454. $wrapper
  2455. .addClass(classes.overlayedClass)
  2456. .removeClass(classes.overlayStickyClass);
  2457. }
  2458. }
  2459.  
  2460. config.stickyActive = active;
  2461. }
  2462.  
  2463. // If the header setting to overlay the menu on the collection image
  2464. // is enabled but the collection setting is disabled, we need to undo
  2465. // the init of the sticky nav
  2466. function disableOverlayHeader() {
  2467. $(selectors.wrapper)
  2468. .removeClass(classes.overlayEnabledClass)
  2469. .removeClass(classes.overlayedClass);
  2470. }
  2471.  
  2472. return {
  2473. init: init,
  2474. disableOverlayHeader: disableOverlayHeader,
  2475. toggleThumbMenu: toggleThumbMenu,
  2476. unload: unload
  2477. };
  2478. })();
  2479.  
  2480. theme.slideNav = (function() {
  2481.  
  2482. var selectors = {
  2483. container: '#PageContainer',
  2484. navWrapper: '.slide-nav__overflow',
  2485. nav: '#SlideNav',
  2486. toggleBtn: '.js-toggle-slide-nav',
  2487. subNavToggleBtn: '.js-toggle-submenu',
  2488. thumbNavToggle: '.site-nav__thumb-button'
  2489. };
  2490.  
  2491. var classes = {
  2492. subNavLink: 'slide-nav__sublist-link',
  2493. return: 'slide-nav__return-btn',
  2494. isActive: 'is-active',
  2495. isOpen: 'js-menu--is-open',
  2496. subNavShowing: 'sub-nav--is-open',
  2497. thirdNavShowing: 'third-nav--is-open'
  2498. };
  2499.  
  2500. var namespace = '.slideNav';
  2501.  
  2502. var isTransitioning;
  2503. var $activeSubNav;
  2504. var $activeTrigger;
  2505. var pageSlide = true;
  2506. var menuLevel = 1;
  2507.  
  2508. function init() {
  2509. if ($(selectors.thumbNavToggle).length) {
  2510. pageSlide = false;
  2511. }
  2512.  
  2513. $(selectors.toggleBtn).on('click' + namespace, toggleNav);
  2514. $(selectors.subNavToggleBtn).on('click' + namespace, toggleSubNav);
  2515. }
  2516.  
  2517. function toggleNav() {
  2518. if ($(selectors.toggleBtn).hasClass(classes.isActive)) {
  2519. closeNav();
  2520. } else {
  2521. openNav();
  2522. }
  2523. }
  2524.  
  2525. function openNav() {
  2526. $(selectors.toggleBtn).addClass(classes.isActive);
  2527.  
  2528. $(selectors.navWrapper).prepareTransition().addClass(classes.isOpen);
  2529.  
  2530. if (pageSlide) {
  2531. $(selectors.container).css({
  2532. transform:
  2533. 'translate3d(0, ' + $(selectors.navWrapper).height() + 'px, 0)'
  2534. });
  2535. }
  2536.  
  2537. $(selectors.navWrapper).attr('tabindex', '-1').focus();
  2538.  
  2539. // close on escape
  2540. $(window).on('keyup' + namespace, function(evt) {
  2541. if (evt.which === 27) {
  2542. closeNav();
  2543. }
  2544. });
  2545. }
  2546.  
  2547. function closeNav() {
  2548. $(selectors.toggleBtn).removeClass(classes.isActive);
  2549. $(selectors.navWrapper).prepareTransition().removeClass(classes.isOpen);
  2550.  
  2551. if (pageSlide) {
  2552. $(selectors.container).removeAttr('style');
  2553. }
  2554.  
  2555. $(selectors.toggleBtn).focus();
  2556.  
  2557. $(window).off('keyup' + namespace);
  2558. }
  2559.  
  2560. function toggleSubNav(evt) {
  2561. if (isTransitioning) {
  2562. return;
  2563. }
  2564.  
  2565. var $toggleBtn = $(evt.currentTarget);
  2566. var isReturn = $toggleBtn.hasClass(classes.return);
  2567. isTransitioning = true;
  2568.  
  2569. if (isReturn) {
  2570. // Close all subnavs by removing active class on buttons
  2571. $(
  2572. classes.toggleBtn + '[data-level="' + (menuLevel - 1) + '"]'
  2573. ).removeClass(classes.isActive);
  2574. $('.slide-nav__dropdown[data-level="' + (menuLevel) + '"]').prepareTransition().removeClass(classes.isActive);
  2575.  
  2576. if ($activeTrigger && $activeTrigger.length) {
  2577. $activeTrigger.removeClass(classes.isActive);
  2578. }
  2579. } else {
  2580. $toggleBtn.addClass(classes.isActive);
  2581. $toggleBtn.next('.slide-nav__dropdown').prepareTransition().addClass(classes.isActive);
  2582. }
  2583.  
  2584. $activeTrigger = $toggleBtn;
  2585.  
  2586. goToSubnav($toggleBtn.data('target'));
  2587. }
  2588.  
  2589. function goToSubnav(target) {
  2590. var $targetMenu = target
  2591. ? $('.slide-nav__dropdown[data-parent="' + target + '"]')
  2592. : $(selectors.nav);
  2593.  
  2594. menuLevel = $targetMenu.data('level') ? $targetMenu.data('level') : 1;
  2595.  
  2596. $activeSubNav = $targetMenu;
  2597.  
  2598. var $elementToFocus = target
  2599. ? $targetMenu.find('.' + classes.subNavLink + ':first')
  2600. : $activeTrigger;
  2601.  
  2602. var translateMenuHeight = $targetMenu.outerHeight();
  2603.  
  2604. var openNavClass =
  2605. menuLevel > 2 ? classes.thirdNavShowing : classes.subNavShowing;
  2606.  
  2607. $(selectors.navWrapper)
  2608. .css('height', translateMenuHeight)
  2609. .removeClass(classes.thirdNavShowing)
  2610. .addClass(openNavClass);
  2611.  
  2612. if (!target) {
  2613. // Show top level nav
  2614. $(selectors.navWrapper)
  2615. .removeClass(classes.thirdNavShowing)
  2616. .removeClass(classes.subNavShowing);
  2617. }
  2618.  
  2619. isTransitioning = false;
  2620.  
  2621. // Match height of subnav
  2622. if (pageSlide) {
  2623. $(selectors.container).css({
  2624. transform: 'translate3d(0, ' + translateMenuHeight + 'px, 0)'
  2625. });
  2626. }
  2627. }
  2628.  
  2629. function unload() {
  2630. $(window).off(namespace);
  2631. $(selectors.toggleBtn).off(namespace);
  2632. $(selectors.subNavToggleBtn).off(namespace);
  2633. }
  2634.  
  2635. return {
  2636. init: init,
  2637. unload: unload
  2638. };
  2639. })();
  2640.  
  2641. theme.articleImages = (function() {
  2642.  
  2643. var cache = {};
  2644.  
  2645. function init() {
  2646. cache.$rteImages = $('.rte--indented-images');
  2647.  
  2648. if (!cache.$rteImages.length) {
  2649. return;
  2650. }
  2651.  
  2652. $(window).on('load', setImages);
  2653. }
  2654.  
  2655. function setImages() {
  2656. cache.$rteImages.find('img').each(function() {
  2657. var $el = $(this);
  2658. var attr = $el.attr('style');
  2659.  
  2660. // Check if undefined or float: none
  2661. if (!attr || attr == 'float: none;') {
  2662. // Remove grid-breaking styles if image isn't wider than parent
  2663. if ($el.width() < cache.$rteImages.width()) {
  2664. $el.addClass('rte__no-indent');
  2665. }
  2666. }
  2667. });
  2668. }
  2669.  
  2670. return {
  2671. init: init
  2672. };
  2673. })();
  2674.  
  2675. theme.Slideshow = (function() {
  2676. this.$slideshow = null;
  2677.  
  2678. var classes = {
  2679. next: 'is-next',
  2680. init: 'is-init',
  2681. wrapper: 'slideshow-wrapper',
  2682. slideshow: 'slideshow',
  2683. currentSlide: 'slick-current',
  2684. pauseButton: 'slideshow__pause',
  2685. isPaused: 'is-paused'
  2686. };
  2687.  
  2688. function slideshow(el, args) {
  2689. this.$slideshow = $(el);
  2690. this.$wrapper = this.$slideshow.closest('.' + classes.wrapper);
  2691. this.$pause = this.$wrapper.find('.' + classes.pauseButton);
  2692.  
  2693. this.settings = {
  2694. accessibility: true,
  2695. arrows: args.arrows ? true : false,
  2696. dots: args.dots ? true : false,
  2697. draggable: true,
  2698. touchThreshold: 8,
  2699. speed: 300,
  2700. pauseOnHover: args.pauseOnHover ? true : false,
  2701. autoplay: this.$slideshow.data('autoplay'),
  2702. autoplaySpeed: this.$slideshow.data('speed')
  2703. };
  2704.  
  2705. this.$slideshow.on('init', this.init.bind(this));
  2706.  
  2707. // Refresh main page slideshow
  2708. if ($('.root').find(this.$slideshow).length) {
  2709. $('body').on('productModalClose', function() {
  2710. this.$slideshow.addClass('slideshow-refresh');
  2711. this.$slideshow.slick('refresh');
  2712. }.bind(this));
  2713. }
  2714.  
  2715. this.$slideshow.slick(this.settings);
  2716.  
  2717. this.$pause.on('click', this._togglePause.bind(this));
  2718. }
  2719.  
  2720. slideshow.prototype = $.extend({}, slideshow.prototype, {
  2721. init: function(event, obj) {
  2722. this.$slideshowList = obj.$list;
  2723. this.$slickDots = obj.$dots;
  2724. this.$allSlides = obj.$slides;
  2725. this.slideCount = obj.slideCount;
  2726.  
  2727. this.$slideshow.addClass(classes.init);
  2728. this._a11y();
  2729. this._clonedLazyloading();
  2730. },
  2731. destroy: function() {
  2732. this.$slideshow.slick('unslick');
  2733. },
  2734.  
  2735. // Playback
  2736. _play: function() {
  2737. this.$slideshow.slick('slickPause');
  2738. $(classes.pauseButton).addClass('is-paused');
  2739. },
  2740. _pause: function() {
  2741. this.$slideshow.slick('slickPlay');
  2742. $(classes.pauseButton).removeClass('is-paused');
  2743. },
  2744. _togglePause: function() {
  2745. var slideshowSelector = this._getSlideshowId(this.$pause);
  2746. if (this.$pause.hasClass(classes.isPaused)) {
  2747. this.$pause.removeClass(classes.isPaused);
  2748. $(slideshowSelector).slick('slickPlay');
  2749. } else {
  2750. this.$pause.addClass(classes.isPaused);
  2751. $(slideshowSelector).slick('slickPause');
  2752. }
  2753. },
  2754.  
  2755. // Helpers
  2756. _getSlideshowId: function($el) {
  2757. return '#Slideshow-' + $el.data('id');
  2758. },
  2759. _activeSlide: function() {
  2760. return this.$slideshow.find('.slick-active');
  2761. },
  2762. _currentSlide: function() {
  2763. return this.$slideshow.find('.slick-current');
  2764. },
  2765. _nextSlide: function(index) {
  2766. return this.$slideshow.find('.slideshow__slide[data-slick-index="' + index + '"]');
  2767. },
  2768.  
  2769. // a11y fixes
  2770. _a11y: function() {
  2771. var $list = this.$slideshowList;
  2772. var autoplay = this.settings.autoplay;
  2773.  
  2774. if (!$list) {
  2775. return;
  2776. }
  2777.  
  2778. // Remove default Slick aria-live attr until slider is focused
  2779. $list.removeAttr('aria-live');
  2780.  
  2781. // When an element in the slider is focused
  2782. // pause slideshow and set aria-live
  2783. $(classes.wrapper).on('focusin', function(evt) {
  2784. if (!$(classes.wrapper).has(evt.target).length) {
  2785. return;
  2786. }
  2787.  
  2788. $list.attr('aria-live', 'polite');
  2789. if (autoplay) {
  2790. this._pause();
  2791. }
  2792. }.bind(this));
  2793.  
  2794. // Resume autoplay
  2795. $(classes.wrapper).on('focusout', function(evt) {
  2796. if (!$(classes.wrapper).has(evt.target).length) {
  2797. return;
  2798. }
  2799.  
  2800. $list.removeAttr('aria-live');
  2801. if (autoplay) {
  2802. this._play();
  2803. }
  2804. }.bind(this));
  2805. },
  2806.  
  2807. // Make sure lazyloading works on cloned slides
  2808. _clonedLazyloading: function() {
  2809. var $slideshow = this.$slideshow;
  2810.  
  2811. $slideshow.find('.slick-slide').each(function(index, el) {
  2812. var $slide = $(el);
  2813. if ($slide.hasClass('slick-cloned')) {
  2814. var slideId = $slide.data('id');
  2815. var $slideImg = $slide.find('.hero__image').removeClass('lazyloading').addClass('lazyloaded');
  2816.  
  2817. // Get inline style attribute from non-cloned slide with arbitrary timeout
  2818. // so the image is loaded
  2819. setTimeout(function() {
  2820. var loadedImageStyle = $slideshow.find('.slideshow__slide--' + slideId + ':not(.slick-cloned) .hero__image').attr('style');
  2821.  
  2822. if (loadedImageStyle) {
  2823. $slideImg.attr('style', loadedImageStyle);
  2824. }
  2825.  
  2826. }, this.settings.autoplaySpeed / 1.5);
  2827.  
  2828. }
  2829. }.bind(this));
  2830. }
  2831. });
  2832.  
  2833. return slideshow;
  2834. })();
  2835.  
  2836. theme.currencySwitcher = (function() {
  2837.  
  2838. var selectors = {
  2839. dataDiv: '#CurrencyData',
  2840. currencyOptions: '.currency-options__btn',
  2841. pickerFlag: '#CurrencyFlag',
  2842. pickerLabel: '#CurrencyLabel'
  2843. };
  2844.  
  2845. var data = {};
  2846. var modal;
  2847.  
  2848. function init() {
  2849. var $dataDiv = $(selectors.dataDiv);
  2850.  
  2851. if (!$dataDiv.length) {
  2852. return;
  2853. }
  2854.  
  2855. modal = new theme.Modals('CurrencyModal', 'currency-modal', {
  2856. closeOffContentClick: false
  2857. });
  2858.  
  2859. $(selectors.currencyOptions).on('click', setNewCurrency);
  2860.  
  2861. data = {
  2862. currency: $dataDiv.data('shop-currency'),
  2863. format: $dataDiv.data('format'),
  2864. moneyFormat: $dataDiv.data('money-format'),
  2865. moneyCurrencyFormat: $dataDiv.data('money-currency-format')
  2866. };
  2867.  
  2868. if (!theme.settings.nativeMultiCurrency) {
  2869. Currency.format = data.format;
  2870.  
  2871. // Rely on the shop's currency format, not Shopify defaults (in case merchant changes it)
  2872. Currency.money_format[data.currency] = data.moneyFormat;
  2873. Currency.money_with_currency_format[data.currency] = data.moneyCurrencyFormat;
  2874.  
  2875. // Fix for customer account page
  2876. $('span.money span.money').each(function() {
  2877. $(this).parents('span.money').removeClass('money');
  2878. });
  2879.  
  2880. // Save current price
  2881. $('span.money').each(function() {
  2882. $(this).attr('data-currency-' + data.currency, $(this).html());
  2883. });
  2884.  
  2885. checkCookie();
  2886. }
  2887. }
  2888.  
  2889. function setNewCurrency() {
  2890. var newCurrency = $(this).data('value');
  2891.  
  2892. if (theme.settings.nativeMultiCurrency) {
  2893. $(this).addClass('is-active');
  2894. theme.cart.updateCurrency(newCurrency);
  2895. return;
  2896. }
  2897.  
  2898. if (newCurrency !== data.currency) {
  2899. data.currency = newCurrency;
  2900. $(selectors.dataDiv).data('current-currency', newCurrency);
  2901. updatePicker(newCurrency);
  2902.  
  2903. refresh();
  2904. }
  2905.  
  2906. modal.close();
  2907. }
  2908.  
  2909. function updatePicker(currency) {
  2910. $(selectors.pickerFlag).attr('data-flag', currency);
  2911. $(selectors.pickerLabel).text(currency);
  2912.  
  2913. // Update modal options active states
  2914. $(selectors.currencyOptions).removeClass('is-active');
  2915. $(selectors.currencyOptions + '[data-value=' + currency + ']').addClass('is-active');
  2916. }
  2917.  
  2918. // Refresh functions only needed when not using native multi-currency
  2919. function refresh() {
  2920. if (theme.settings.nativeMultiCurrency) {
  2921. return;
  2922. }
  2923.  
  2924. var newCurrency = $(selectors.dataDiv).data('current-currency');
  2925. Currency.convertAll(Currency.currentCurrency, newCurrency);
  2926. }
  2927.  
  2928. function ajaxrefresh() {
  2929. if (theme.settings.nativeMultiCurrency) {
  2930. return;
  2931. }
  2932.  
  2933. var shopCurrency = $(selectors.dataDiv).data('shop-currency');
  2934. var newCurrency = $(selectors.dataDiv).data('current-currency');
  2935. // Ajax cart always returns shop's currency, not what theme settings defines
  2936. Currency.convertAll(shopCurrency, newCurrency);
  2937. }
  2938.  
  2939. function checkCookie() {
  2940. var cookieCurrency = Currency.cookie.read();
  2941.  
  2942. if (cookieCurrency == null) {
  2943. Currency.currentCurrency = cookieCurrency = data.currency;
  2944. } else if ($(selectors.currencyOptions).length && $(selectors.currencyOptions + '[data-value=' + cookieCurrency + ']').length === 0) {
  2945. // If the cookie value does not correspond to any value in the currency dropdown
  2946. Currency.currentCurrency = data.currency;
  2947. Currency.cookie.write(data.currency);
  2948. } else if (cookieCurrency === data.currency) {
  2949. Currency.currentCurrency = data.currency;
  2950. } else {
  2951. Currency.convertAll(data.currency, cookieCurrency);
  2952. }
  2953.  
  2954. // Update current currency with cookie value
  2955. $(selectors.dataDiv).data('current-currency', cookieCurrency);
  2956. data.currency = cookieCurrency;
  2957. updatePicker(cookieCurrency);
  2958. }
  2959.  
  2960. return {
  2961. init: init,
  2962. refresh: refresh,
  2963. ajaxrefresh: ajaxrefresh
  2964. };
  2965. })();
  2966.  
  2967. /*
  2968. Quick shop modals, or product screens, live inside
  2969. product-grid-item markup until page load, where they're
  2970. moved to #ProductScreens at the bottom of the page
  2971. */
  2972.  
  2973. theme.QuickShopScreens = (function() {
  2974.  
  2975. var startingUrl = window.location.pathname;
  2976. var currentPath = startingUrl;
  2977. var prevPath = null;
  2978. var currentScreen = null;
  2979. if ('scrollRestoration' in history) {
  2980. history.scrollRestoration = 'manual';
  2981. }
  2982.  
  2983. var selectors = {
  2984. screensWrap: '#ProductScreens',
  2985. screens: '[data-product-id]',
  2986. trigger: '.quick-product__btn'
  2987. };
  2988.  
  2989. var activeIds = [];
  2990.  
  2991. function init(container) {
  2992. if (!theme.settings.quickView) {
  2993. return;
  2994. }
  2995.  
  2996. var productIds = getProductIds();
  2997. initProductScreens(productIds);
  2998. initHistoryWatcher();
  2999. }
  3000.  
  3001. function initHistoryWatcher() {
  3002. // No need to adjust URL in the editor since it handles the navigation
  3003. if (Shopify.designMode) {
  3004. return;
  3005. }
  3006.  
  3007. // Listen for product screens opening
  3008. $(window).on('newPopstate', function(evt, data) {
  3009. currentScreen = data.screen;
  3010. // Manually trigger back, comes from esc key or close btns
  3011. if (data.back) {
  3012. prevPath = location.pathname;
  3013. currentPath = startingUrl;
  3014. history.pushState({}, '', startingUrl);
  3015. }
  3016.  
  3017. if (data.url) {
  3018. if (data.updateCurrentPath) {
  3019. prevPath = location.pathname;
  3020. currentPath = data.url;
  3021. history.pushState({}, '', data.url);
  3022. }
  3023. }
  3024. });
  3025.  
  3026. $(window).on('popstate', function(evt) {
  3027. var goToUrl = false;
  3028. prevPath = currentPath;
  3029.  
  3030. // Hash change or no change, let browser take over
  3031. if (location.pathname === currentPath) {
  3032. return;
  3033. }
  3034.  
  3035. prevPath = currentPath;
  3036. currentPath = location.pathname;
  3037.  
  3038. // Back to where we started. Close existing screen if open
  3039. if (location.pathname === startingUrl) {
  3040. if (currentScreen && currentScreen.isOpen) {
  3041. closeScreen(currentScreen);
  3042. }
  3043. return;
  3044. }
  3045.  
  3046. // Opening product
  3047. if (location.pathname.indexOf('/products/') !== -1) {
  3048. if (currentScreen) {
  3049. currentScreen.open();
  3050. } else {
  3051. // No screen sent to function, trigger new click
  3052. $('.quick-product__btn[href="'+ location.pathname +'"]').first().trigger('click', { updateCurrentPath: false });
  3053. }
  3054.  
  3055. return;
  3056. }
  3057.  
  3058. if (evt.originalEvent.state) {
  3059. if (currentScreen && currentScreen.isOpen) {
  3060. closeScreen(currentScreen);
  3061. history.replaceState({}, '', startingUrl);
  3062. return;
  3063. }
  3064.  
  3065. goToUrl = true;
  3066. } else {
  3067. if (currentScreen) {
  3068. if (currentScreen.isOpen) {
  3069. closeScreen(currentScreen);
  3070. return;
  3071. }
  3072. } else {
  3073. // No state/modal. Navigate to where browser wants
  3074. goToUrl = true;
  3075. }
  3076. }
  3077.  
  3078. // Fallback if none of our conditions are met
  3079. if (goToUrl) {
  3080. window.location.href = location.href;
  3081. }
  3082. }.bind(this));
  3083. }
  3084.  
  3085. function closeScreen(screen) {
  3086. screen.close();
  3087. currentScreen = null;
  3088. $(window).trigger('resize');
  3089. }
  3090.  
  3091. function getProductIds($scope) {
  3092. var ids = [];
  3093.  
  3094. var $triggers = $scope ? $(selectors.trigger, $scope) : $(selectors.trigger);
  3095.  
  3096. $triggers.each(function() {
  3097. var id = $(this).data('product-id');
  3098.  
  3099. // If another identical modal exists, remove from DOM
  3100. if (ids.indexOf(id) > -1) {
  3101. $('.screen-layer--product[data-product-id="' + id + '"]').slice(1).remove();
  3102. return;
  3103. }
  3104.  
  3105. ids.push(id);
  3106. });
  3107.  
  3108. return ids;
  3109. }
  3110.  
  3111. function getIdsFromTriggers($triggers) {
  3112. var ids = [];
  3113.  
  3114. $triggers.each(function() {
  3115. var id = $(this).data('product-id');
  3116. ids.push(id);
  3117. });
  3118.  
  3119. return ids;
  3120. }
  3121.  
  3122. function initProductScreens(ids) {
  3123. var screenId;
  3124. var $screenLayer;
  3125. var screens = [];
  3126.  
  3127. // Init screens if they're not duplicates
  3128. for (var i = 0; i < ids.length; i++) {
  3129. if (activeIds.indexOf(ids[i]) === -1) {
  3130. screenId = 'ProductScreen-' + ids[i];
  3131. $screenLayer = $('#' + screenId);
  3132.  
  3133. screens.push($screenLayer);
  3134. activeIds.push(ids[i]);
  3135. new theme.ProductScreen(screenId, 'product-' + ids[i]);
  3136. }
  3137. }
  3138.  
  3139. // Append screens to bottom of page
  3140. $(selectors.screensWrap).append(screens);
  3141. }
  3142.  
  3143. // Section unloaded in theme editor.
  3144. // Check if product exists in any other area
  3145. // of the page, remove other's section.instance
  3146. function unload($container) {
  3147. if (!theme.settings.quickView) {
  3148. return;
  3149. }
  3150.  
  3151. var removeIds = [];
  3152. var productIds = getProductIds($container);
  3153.  
  3154. // Get ids from buttons not in removed section
  3155. var $activeButtons = $(selectors.trigger).not($(selectors.trigger, $container));
  3156. var stillActiveIds = getIdsFromTriggers($activeButtons);
  3157.  
  3158. // If ID exists on active button, do not add to IDs to remove
  3159. for (var i = 0; i < productIds.length; i++) {
  3160. var id = productIds[i];
  3161. if (stillActiveIds.indexOf(id) === -1) {
  3162. removeIds.push(id);
  3163. }
  3164. }
  3165.  
  3166. for (var i = 0; i < removeIds.length; i++) {
  3167. sections._removeInstance(removeIds[i]);
  3168. }
  3169. }
  3170.  
  3171. // Section container is sent, so must re-scrape for product IDs
  3172. function reInit($container) {
  3173. if (!theme.settings.quickView) {
  3174. return;
  3175. }
  3176.  
  3177. var newProductIds = getProductIds($container);
  3178. initProductScreens(newProductIds);
  3179. removeDuplicateModals(newProductIds, $container);
  3180.  
  3181. // Re-register product templates in quick view modals.
  3182. // Will not double-register.
  3183. sections.register('product-template', theme.Product, $('#ProductScreens'));
  3184. }
  3185.  
  3186. function removeDuplicateModals(ids, $container) {
  3187. for (var i = 0; i < ids.length; i++) {
  3188. $('.screen-layer--product[data-product-id="' + ids[i] + '"]', $container).remove();
  3189. }
  3190. }
  3191.  
  3192. return {
  3193. init: init,
  3194. unload: unload,
  3195. reInit: reInit
  3196. };
  3197. })();
  3198.  
  3199. /*
  3200. Hover to enable slideshow of product images.
  3201. On mobile slideshow starts as item is in view.
  3202. Destroy on mouseout/out of view.
  3203. */
  3204.  
  3205. theme.HoverProductGrid = (function() {
  3206. var selectors = {
  3207. product: '.grid-product',
  3208. slider: '.product-slider',
  3209. };
  3210.  
  3211. function HoverProductGrid($container) {
  3212. this.$container = $container;
  3213. this.sectionId = this.$container.attr('data-section-id');
  3214. this.namespace = '.product-image-slider-' + this.sectionId;
  3215. this.activeIds = [];
  3216.  
  3217. if (!theme.settings.hoverProductGrid) {
  3218. return;
  3219. }
  3220.  
  3221. this.$products = $container.find(selectors.product);
  3222. this.slidersMobile = $container.data('product-sliders-mobile');
  3223.  
  3224. // No products means no sliders
  3225. if (this.$products.length === 0) {
  3226. return;
  3227. }
  3228.  
  3229. slate.utils.promiseStylesheet().then(function() {
  3230. this.init();
  3231. }.bind(this));
  3232. }
  3233.  
  3234. HoverProductGrid.prototype = $.extend({}, HoverProductGrid.prototype, {
  3235. init: function() {
  3236. this.destroyAllSliders();
  3237. this.setupEventType();
  3238. this.listnerSetup();
  3239. },
  3240.  
  3241. setupEventType: function() {
  3242. this.$products.off('mouseenter mouseout');
  3243. $(window).off('scroll' + this.namespace);
  3244.  
  3245. if (theme.config.bpSmall) {
  3246. if (this.slidersMobile) {
  3247. $(window).on('scroll' + this.namespace, $.throttle(120, this.inViewSliderInit.bind(this)));
  3248. $(window).trigger('scroll' + this.namespace);
  3249. }
  3250. } else {
  3251. this.mouseSliderInit();
  3252. }
  3253. },
  3254.  
  3255. listnerSetup: function() {
  3256. $('body').on('matchSmall matchLarge', function() {
  3257. this.destroyAllSliders();
  3258. this.setupEventType();
  3259. }.bind(this));
  3260. },
  3261.  
  3262. inViewSliderInit: function() {
  3263. this.$products.find(selectors.slider).each(function(i, el) {
  3264. if(theme.isElementVisible($(el), -400)) {
  3265. this.initSlider($(el));
  3266. } else {
  3267. this.destroySlider($(el));
  3268. }
  3269. }.bind(this));
  3270. },
  3271.  
  3272. mouseSliderInit: function() {
  3273. this.$products.on('mouseenter', function(evt) {
  3274. var $slider = $(evt.currentTarget).find(selectors.slider);
  3275. this.initSlider($slider);
  3276. }.bind(this));
  3277.  
  3278. this.$products.on('mouseleave', function(evt) {
  3279. var $slider = $(evt.currentTarget).find(selectors.slider);
  3280. this.destroySlider($slider);
  3281. }.bind(this));
  3282. },
  3283.  
  3284. initSlider: function($slider) {
  3285. if ($slider.data('image-count') < 2) {
  3286. return;
  3287. }
  3288.  
  3289. if (this.activeIds.indexOf($slider.data('id')) !== -1) {
  3290. return;
  3291. }
  3292.  
  3293. this.activeIds.push($slider.data('id'));
  3294.  
  3295. $slider
  3296. .addClass('product-slider--init')
  3297. .slick({
  3298. autoplay: true,
  3299. infinite: true,
  3300. arrows: false,
  3301. speed: 300,
  3302. fade: true,
  3303. pauseOnHover: false,
  3304. autoplaySpeed: 1050
  3305. });
  3306. },
  3307.  
  3308. destroySlider: function($slider) {
  3309. if ($slider.data('image-count') < 2) {
  3310. return;
  3311. }
  3312.  
  3313. var alreadyActive = this.activeIds.indexOf($slider.data('id'));
  3314. if (alreadyActive !== -1) {
  3315. this.activeIds.splice(alreadyActive, 1);
  3316. $slider.slick('unslick');
  3317. }
  3318. },
  3319.  
  3320. destroyAllSliders: function() {
  3321. this.$products.find(selectors.slider).each(function(i, el) {
  3322. this.destroySlider($(el));
  3323. }.bind(this));
  3324. }
  3325. });
  3326.  
  3327. return HoverProductGrid;
  3328. })();
  3329.  
  3330. theme.videoModal = function() {
  3331. var videoModalPlayer = null;
  3332. var videoOptions = {
  3333. width: 1280,
  3334. height: 720,
  3335. playerVars: {
  3336. autohide: 0,
  3337. autoplay: 1,
  3338. branding: 0,
  3339. cc_load_policy: 0,
  3340. fs: 0,
  3341. iv_load_policy: 3,
  3342. modestbranding: 1,
  3343. playsinline: 1,
  3344. quality: 'hd720',
  3345. rel: 0,
  3346. showinfo: 0,
  3347. wmode: 'opaque'
  3348. }
  3349. };
  3350.  
  3351. var selectors = {
  3352. triggers: 'a[href*="youtube.com/watch"], a[href*="youtu.be/"]'
  3353. };
  3354.  
  3355. if (!$(selectors.triggers).length) {
  3356. return;
  3357. }
  3358.  
  3359. var modal = new theme.Modals('VideoModal', 'video-modal', {
  3360. closeOffContentClick: true
  3361. });
  3362.  
  3363. $(selectors.triggers).on('click', triggerYouTubeModal);
  3364.  
  3365. function triggerYouTubeModal(evt) {
  3366. evt.preventDefault();
  3367. window.loadYouTube();
  3368.  
  3369. if (theme.config.youTubeReady) {
  3370. startVideoOnClick(evt);
  3371. } else {
  3372. $('body').on('youTubeReady', function() {
  3373. startVideoOnClick(evt);
  3374. });
  3375. }
  3376. }
  3377.  
  3378. function startVideoOnClick(evt) {
  3379. var $el = $(evt.target);
  3380.  
  3381. // get video ID from URL
  3382. var videoId = getYoutubeVideoId($el.attr('href'));
  3383.  
  3384. var args = $.extend({}, videoOptions, {
  3385. videoId: videoId
  3386. });
  3387.  
  3388. // Disable plays inline on mobile
  3389. args.playerVars.playsinline = theme.config.bpSmall ? 0 : 1;
  3390.  
  3391. var videoModalPlayer = new YT.Player('VideoHolder', args);
  3392. modal.open();
  3393.  
  3394. $('body').on('modalClose.VideoModal', function() {
  3395. // Slight timeout so it is destroyed after the modal closes
  3396. setTimeout(function() {
  3397. videoModalPlayer.destroy();
  3398. }, 500); // modal close css transition
  3399. });
  3400. }
  3401.  
  3402. function getYoutubeVideoId(url) {
  3403. var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
  3404. var match = url.match(regExp);
  3405. return (match&&match[7].length==11)? match[7] : false;
  3406. }
  3407. };
  3408.  
  3409.  
  3410.  
  3411. theme.customerTemplates = (function() {
  3412.  
  3413. function initEventListeners() {
  3414. // Show reset password form
  3415. $('#RecoverPassword').on('click', function(evt) {
  3416. evt.preventDefault();
  3417. toggleRecoverPasswordForm();
  3418. });
  3419.  
  3420. // Hide reset password form
  3421. $('#HideRecoverPasswordLink').on('click', function(evt) {
  3422. evt.preventDefault();
  3423. toggleRecoverPasswordForm();
  3424. });
  3425. }
  3426.  
  3427. /**
  3428. *
  3429. * Show/Hide recover password form
  3430. *
  3431. */
  3432. function toggleRecoverPasswordForm() {
  3433. $('#RecoverPasswordForm').toggleClass('hide');
  3434. $('#CustomerLoginForm').toggleClass('hide');
  3435. }
  3436.  
  3437. /**
  3438. *
  3439. * Show reset password success message
  3440. *
  3441. */
  3442. function resetPasswordSuccess() {
  3443. var $formState = $('.reset-password-success');
  3444.  
  3445. // check if reset password form was successfully submitted
  3446. if (!$formState.length) {
  3447. return;
  3448. }
  3449.  
  3450. // show success message
  3451. $('#ResetSuccess').removeClass('hide');
  3452. }
  3453.  
  3454. /**
  3455. *
  3456. * Show/hide customer address forms
  3457. *
  3458. */
  3459. function customerAddressForm() {
  3460. var $newAddressForm = $('#AddressNewForm');
  3461. var $addressForms = $('.js-address-form');
  3462.  
  3463. if (!$newAddressForm.length || !$addressForms.length) {
  3464. return;
  3465. }
  3466.  
  3467. if (Shopify) {
  3468. $('.js-address-country').each(function() {
  3469. var $container = $(this);
  3470. var countryId = $container.data('country-id');
  3471. var provinceId = $container.data('province-id');
  3472. var provinceContainerId = $container.data('province-container-id');
  3473.  
  3474. new Shopify.CountryProvinceSelector(
  3475. countryId,
  3476. provinceId,
  3477. {
  3478. hideElement: provinceContainerId
  3479. }
  3480. );
  3481. });
  3482. }
  3483.  
  3484. // Toggle new/edit address forms
  3485. $('.address-new-toggle').on('click', function() {
  3486. $newAddressForm.toggleClass('hide');
  3487. });
  3488.  
  3489. $('.address-edit-toggle').on('click', function() {
  3490. var formId = $(this).data('form-id');
  3491. $('#EditAddress_' + formId).toggleClass('hide');
  3492. });
  3493.  
  3494. $('.address-delete').on('click', function() {
  3495. var $el = $(this);
  3496. var formId = $el.data('form-id');
  3497. var confirmMessage = $el.data('confirm-message');
  3498.  
  3499. if (confirm(confirmMessage || 'Are you sure you wish to delete this address?')) {
  3500. Shopify.postLink('/account/addresses/' + formId, {parameters: {_method: 'delete'}});
  3501. }
  3502. });
  3503. }
  3504.  
  3505. /**
  3506. *
  3507. * Check URL for reset password hash
  3508. *
  3509. */
  3510. function checkUrlHash() {
  3511. var hash = window.location.hash;
  3512.  
  3513. // Allow deep linking to recover password form
  3514. if (hash === '#recover') {
  3515. toggleRecoverPasswordForm();
  3516. }
  3517. }
  3518.  
  3519. return {
  3520. init: function() {
  3521. checkUrlHash();
  3522. initEventListeners();
  3523. resetPasswordSuccess();
  3524. customerAddressForm();
  3525. }
  3526. };
  3527. })();
  3528.  
  3529.  
  3530. theme.Product = (function() {
  3531.  
  3532. var classes = {
  3533. onSale: 'sale-price',
  3534. disabled: 'disabled',
  3535. isModal: 'is-modal',
  3536. loading: 'loading',
  3537. loaded: 'loaded',
  3538. interactable: 'video-interactable',
  3539. visuallyHide: 'visually-invisible',
  3540. thumbActive: 'thumb--current'
  3541. };
  3542.  
  3543. var selectors = {
  3544. variantsJson: '[data-variant-json]',
  3545. currentVariantJson: '[data-current-variant-json]',
  3546.  
  3547. imageContainer: '[data-product-images]',
  3548. mainSlider: '[data-product-photos]',
  3549. thumbSlider: '[data-product-thumbs]',
  3550. photo: '[data-product-photo]',
  3551. photoThumbs: '[data-product-thumb]',
  3552. photoThumbItem: '[data-product-thumb-item]',
  3553.  
  3554. priceWrapper: '[data-price-wrapper]',
  3555. price: '[data-product-price]',
  3556. comparePrice: '[data-product-price-compare]',
  3557. priceA11y: '[data-price-a11y]',
  3558. comparePriceA11y: '[data-compare-a11y]',
  3559. sku: '[data-sku]',
  3560. inventory: '[data-product-inventory]',
  3561. incomingInventory: '[data-product-incoming-inventory]',
  3562. unitWrapper: '[data-product-unit-wrapper]',
  3563.  
  3564. addToCart: '[data-add-to-cart]',
  3565. addToCartText: '[data-add-to-cart-text]',
  3566.  
  3567. originalSelectorId: '[data-product-select]',
  3568. singleOptionSelector: '[data-variant-input]',
  3569. variantColorSwatch: '[data-color-swatch]',
  3570.  
  3571. productImageMain: '.product-image-main',
  3572. productVideo: '[data-product-video]',
  3573. videoParent: '.product__video-wrapper',
  3574. currentSlide: '.slick-current',
  3575.  
  3576. modalFormHolder: '#ProductFormPlaceholder-',
  3577. formContainer: '.product-single__form'
  3578. };
  3579.  
  3580. var youtubeReady;
  3581. var videos = {};
  3582. var youtubePlayers = [];
  3583. var youtubeVideoOptions = {
  3584. height: '480',
  3585. width: '850',
  3586. playerVars :{
  3587. autohide: 0,
  3588. autoplay: 1,
  3589. branding: 0,
  3590. cc_load_policy: 0,
  3591. controls: 0,
  3592. fs: 0,
  3593. iv_load_policy: 3,
  3594. modestbranding: 1,
  3595. playsinline: 1,
  3596. quality: 'hd720',
  3597. rel: 0,
  3598. showinfo: 0,
  3599. wmode: 'opaque'
  3600. },
  3601. events: {
  3602. onReady: onVideoPlayerReady,
  3603. onStateChange: onVideoStateChange
  3604. }
  3605. };
  3606.  
  3607. var vimeoReady;
  3608. var vimeoPlayers = [];
  3609. var vimeoVideoOptions = {
  3610. byline: false,
  3611. title: false,
  3612. portrait: false,
  3613. loop: true
  3614. };
  3615.  
  3616. function onVideoPlayerReady(evt) {
  3617. var $player = $(evt.target.a);
  3618. var playerId = $player.attr('id');
  3619. youtubePlayers[playerId] = evt.target; // update stored player
  3620. var player = youtubePlayers[playerId];
  3621.  
  3622. setParentAsLoading($player);
  3623.  
  3624. if (videos[playerId].style === 'muted') {
  3625. youtubePlayers[playerId].mute().playVideo().pauseVideo();
  3626. } else {
  3627. setParentAsLoaded($player);
  3628. }
  3629.  
  3630. // If first slide or only photo, start video
  3631. if ($player.closest(selectors.currentSlide).length || $player.data('image-count') === 1) {
  3632. if (videos[playerId].style === 'muted') {
  3633. youtubePlayers[playerId].playVideo();
  3634. initCheckVisibility(playerId);
  3635. }
  3636. }
  3637. }
  3638.  
  3639. function initCheckVisibility(playerId) {
  3640. if (!playerId) {
  3641. return;
  3642. }
  3643.  
  3644. // Add out of view pausing
  3645. videoVisibilityCheck(playerId);
  3646. $(window).on('scroll.' + playerId, {id: playerId}, $.throttle(150, videoVisibilityCheck));
  3647. }
  3648.  
  3649. function videoVisibilityCheck(id) {
  3650. var playerId;
  3651.  
  3652. if (!id) {
  3653. return;
  3654. }
  3655.  
  3656. if (typeof id === 'string') {
  3657. playerId = id;
  3658. } else {
  3659. // Data comes in as part of the scroll event
  3660. playerId = id.data.id;
  3661. }
  3662.  
  3663. if (theme.isElementVisible($('#' + playerId))) {
  3664. if (videos[playerId] && videos[playerId].style === 'unmuted') {
  3665. return;
  3666. }
  3667. playVisibleVideo(playerId);
  3668. } else {
  3669. pauseHiddenVideo(playerId);
  3670. }
  3671. }
  3672.  
  3673. function playVisibleVideo(id) {
  3674. if (youtubePlayers[id] && typeof youtubePlayers[id].playVideo === 'function') {
  3675. youtubePlayers[id].playVideo();
  3676. }
  3677. }
  3678.  
  3679. function pauseHiddenVideo(id) {
  3680. if (youtubePlayers[id] && typeof youtubePlayers[id].pauseVideo === 'function') {
  3681. youtubePlayers[id].pauseVideo();
  3682. }
  3683. }
  3684.  
  3685. function onVideoStateChange(evt) {
  3686. var $player = $(evt.target.a);
  3687. var playerId = $player.attr('id');
  3688. var player = youtubePlayers[playerId];
  3689.  
  3690. switch (evt.data) {
  3691. case -1: // unstarted
  3692. // Handle low power state on iOS by checking if
  3693. // video is reset to unplayed after attempting to buffer
  3694. if (videos[playerId].attemptedToPlay) {
  3695. setParentAsLoaded($player);
  3696. setVideoToBeInteractedWith($player);
  3697. }
  3698. break;
  3699. case 0: // ended
  3700. player.playVideo();
  3701. break;
  3702. case 1: // playing
  3703. setParentAsLoaded($player);
  3704. break;
  3705. case 3: // buffering
  3706. videos[playerId].attemptedToPlay = true;
  3707. break;
  3708. }
  3709. }
  3710.  
  3711. function setParentAsLoading($el) {
  3712. $el
  3713. .closest(selectors.videoParent)
  3714. .addClass(classes.loading);
  3715. }
  3716.  
  3717. function setParentAsLoaded($el) {
  3718. $el
  3719. .closest(selectors.videoParent)
  3720. .removeClass(classes.loading)
  3721. .addClass(classes.loaded);
  3722. }
  3723.  
  3724. function setVideoToBeInteractedWith($el) {
  3725. $el
  3726. .closest(selectors.videoParent)
  3727. .addClass(classes.interactable);
  3728. }
  3729.  
  3730. function Product(container) {
  3731. var $container = this.$container = $(container);
  3732. var sectionId = this.sectionId = $container.attr('data-section-id');
  3733.  
  3734. this.inModal = $container.closest('.screen-layer').length;
  3735. this.$modal;
  3736.  
  3737. this.namespace = '.product-' + sectionId;
  3738. this.namespaceImages = '.product-image-' + sectionId;
  3739.  
  3740. this.settings = {
  3741. enableHistoryState: $container.data('enable-history-state') || false,
  3742. namespace: '.product-' + sectionId,
  3743. variantType: $container.data('variant-type'),
  3744. inventory: $container.data('inventory') || false,
  3745. inventoryThreshold: $container.data('inventory-threshold') || false,
  3746. incomingInventory: $container.data('incoming-inventory') || false,
  3747. modalInit: false,
  3748. slickMainInitialized: false,
  3749. slickThumbInitialized: false,
  3750. hasImages: true,
  3751. hasVideos: $container.find(selectors.productVideo).length || false,
  3752. hasMultipleImages: false,
  3753. stackedImages: $container.data('images-stacked') || false,
  3754. stackedCurrent: 0,
  3755. stackedImagePositions: [],
  3756. imagesAnimating: false,
  3757. imageSize: '620x'
  3758. };
  3759.  
  3760. // Overwrite some settings when loaded in modal
  3761. if (this.inModal) {
  3762. this.settings.enableHistoryState = false;
  3763. this.namespace = '.product-' + sectionId + '-modal';
  3764. this.$modal = $('#ProductScreen-' + sectionId);
  3765. }
  3766.  
  3767. this.init();
  3768. }
  3769.  
  3770. Product.prototype = $.extend({}, Product.prototype, {
  3771. init: function() {
  3772. this.$mainSlider = $(selectors.mainSlider, this.$container);
  3773. this.$thumbSlider = $(selectors.thumbSlider, this.$container);
  3774. this.$firstProductImage = this.$mainSlider.find('img').first();
  3775. this.$formHolder = $(selectors.modalFormHolder + this.sectionId);
  3776.  
  3777. if (!this.$firstProductImage.length) {
  3778. this.settings.hasImages = false;
  3779. }
  3780.  
  3781. if (this.inModal) {
  3782. this.$container.addClass(classes.isModal);
  3783. $('body')
  3784. .off('productModalOpen.ProductScreen-' + this.sectionId)
  3785. .off('productModalClose.ProductScreen-' + this.sectionId);
  3786. $('body').on('productModalOpen.ProductScreen-' + this.sectionId, this.openModalProduct.bind(this));
  3787. $('body').on('productModalClose.ProductScreen-' + this.sectionId, this.closeModalProduct.bind(this));
  3788. }
  3789.  
  3790. if (!this.inModal) {
  3791. this.stringOverrides();
  3792. this.formSetup();
  3793. this.preImageSetup();
  3794.  
  3795. this.checkIfVideos();
  3796. this.imageSetup(true);
  3797. }
  3798. },
  3799.  
  3800. formSetup: function() {
  3801. // Determine how to handle variant availability selectors
  3802. if (theme.settings.dynamicVariantsEnable) {
  3803. this.$variantSelectors = $(selectors.formContainer, this.$container).find(selectors.singleOptionSelector);
  3804. }
  3805.  
  3806. this.initAjaxProductForm();
  3807. this.initVariants();
  3808. },
  3809.  
  3810. preImageSetup: function() {
  3811. this.setImageSizes();
  3812. this.initImageSwitch();
  3813. this.initImageZoom();
  3814. },
  3815.  
  3816. imageSetup: function(needStylesheet) {
  3817. if (!this.$thumbSlider.length || $(selectors.photoThumbs, this.$container).length < 2) {
  3818. // Single product image. Init video if it exists
  3819. var $video = $(selectors.productImageMain, this.$container).find(selectors.productVideo);
  3820. if ($video.length) {
  3821. this.initVideo($video);
  3822. }
  3823.  
  3824. return;
  3825. }
  3826.  
  3827. this.settings.hasMultipleImages = true;
  3828.  
  3829. if (needStylesheet) {
  3830. slate.utils.promiseStylesheet().then(function() {
  3831. this.createImageCarousels();
  3832. }.bind(this));
  3833. } else {
  3834. this.createImageCarousels();
  3835. }
  3836. },
  3837.  
  3838. initImageZoom: function() {
  3839. var $container = $(selectors.imageContainer, this.$container);
  3840. var imageZoom = new theme.Photoswipe($container, this.sectionId);
  3841. },
  3842.  
  3843. stringOverrides: function() {
  3844. theme.productStrings = theme.productStrings || {};
  3845. $.extend(theme.strings, theme.productStrings);
  3846. },
  3847.  
  3848. setImageSizes: function() {
  3849. if (!this.settings.hasImages) {
  3850. return;
  3851. }
  3852.  
  3853. // Get srcset image src, works on most modern browsers
  3854. // otherwise defaults to settings.imageSize
  3855. var currentImage = this.$firstProductImage[0].currentSrc;
  3856.  
  3857. if (currentImage) {
  3858. this.settings.imageSize = theme.Images.imageSize(currentImage);
  3859. }
  3860. },
  3861.  
  3862. initVariants: function() {
  3863. var $variantJson = $(selectors.variantsJson, this.$container);
  3864. if (!$variantJson.length) {
  3865. return;
  3866. }
  3867.  
  3868. this.variantsObject = JSON.parse($variantJson[0].innerHTML);
  3869.  
  3870. var options = {
  3871. $container: this.$container,
  3872. enableHistoryState: this.settings.enableHistoryState,
  3873. singleOptionSelector: selectors.singleOptionSelector,
  3874. originalSelectorId: selectors.originalSelectorId,
  3875. variants: this.variantsObject
  3876. };
  3877.  
  3878. if ($(selectors.variantColorSwatch, this.$container).length) {
  3879. $(selectors.variantColorSwatch, this.$container).on('change', function(evt) {
  3880. var $el = $(evt.currentTarget);
  3881. var color = $el.data('color-name');
  3882. var index = $el.data('color-index');
  3883. this.updateColorName(color, index);
  3884. }.bind(this));
  3885. }
  3886.  
  3887. this.variants = new slate.Variants(options);
  3888.  
  3889. this.$container
  3890. .on('variantChange' + this.namespace, this.updateCartButton.bind(this))
  3891. .on('variantImageChange' + this.namespace, this.updateVariantImage.bind(this))
  3892. .on('variantPriceChange' + this.namespace, this.updatePrice.bind(this))
  3893. .on('variantUnitPriceChange' + this.namespace, this.updateUnitPrice.bind(this));
  3894.  
  3895. if ($(selectors.sku, this.$container).length) {
  3896. this.$container.on('variantSKUChange' + this.namespace, this.updateSku.bind(this));
  3897. }
  3898. if (this.settings.inventory || this.settings.incomingInventory) {
  3899. this.$container.on('variantChange' + this.namespace, this.updateInventory.bind(this));
  3900. }
  3901.  
  3902. // Update individual variant availability on each selection
  3903. var $currentVariantJson = $(selectors.currentVariantJson, this.$container);
  3904.  
  3905. if (theme.settings.dynamicVariantsEnable && $currentVariantJson.length) {
  3906. this.currentVariantObject = JSON.parse($currentVariantJson[0].innerHTML);
  3907.  
  3908. this.$variantSelectors.on('change' + this.namespace, this.updateVariantAvailability.bind(this));
  3909.  
  3910. // Set default state based on current selected variant
  3911. this.setCurrentVariantAvailability(this.currentVariantObject, true);
  3912. }
  3913. },
  3914.  
  3915. // Variant change functions
  3916. updateColorName: function(color, index) {
  3917. // Updates on radio button change, not variant.js
  3918. $('#VariantColorLabel-' + this.sectionId + '-' + index).text(color);
  3919. },
  3920.  
  3921. updateCartButton: function(evt) {
  3922. var variant = evt.variant;
  3923.  
  3924. if (variant) {
  3925. if (variant.available) {
  3926. // Available, enable the submit button and change text
  3927. $(selectors.addToCart, this.$container).removeClass(classes.disabled).prop('disabled', false);
  3928. $(selectors.addToCartText, this.$container).html(theme.strings.addToCart);
  3929. } else {
  3930. // Sold out, disable the submit button and change text
  3931. $(selectors.addToCart, this.$container).addClass(classes.disabled).prop('disabled', true);
  3932. $(selectors.addToCartText, this.$container).html(theme.strings.soldOut);
  3933. }
  3934. } else {
  3935. // The variant doesn't exist, disable submit button
  3936. $(selectors.addToCart, this.$container).addClass(classes.disabled).prop('disabled', true);
  3937. $(selectors.addToCartText, this.$container).html(theme.strings.unavailable);
  3938. }
  3939. },
  3940.  
  3941. updatePrice: function(evt) {
  3942. var variant = evt.variant;
  3943.  
  3944. if (variant) {
  3945. // Regular price
  3946. $(selectors.price, this.$container).html(theme.Currency.formatMoney(variant.price, theme.settings.moneyFormat)).show();
  3947.  
  3948. // Sale price, if necessary
  3949. if (variant.compare_at_price > variant.price) {
  3950. $(selectors.comparePrice, this.$container).html(theme.Currency.formatMoney(variant.compare_at_price, theme.settings.moneyFormat));
  3951. $(selectors.priceWrapper, this.$container).removeClass('hide');
  3952. $(selectors.price, this.$container).addClass(classes.onSale);
  3953. $(selectors.comparePriceA11y, this.$container).attr('aria-hidden', 'false');
  3954. $(selectors.priceA11y, this.$container).attr('aria-hidden', 'false');
  3955. } else {
  3956. $(selectors.priceWrapper, this.$container).addClass('hide');
  3957. $(selectors.price, this.$container).removeClass(classes.onSale);
  3958. $(selectors.comparePriceA11y, this.$container).attr('aria-hidden', 'true');
  3959. $(selectors.priceA11y, this.$container).attr('aria-hidden', 'true');
  3960. }
  3961.  
  3962. if (theme.settings.currenciesEnabled) {
  3963. theme.currencySwitcher.ajaxrefresh();
  3964. }
  3965. }
  3966. },
  3967.  
  3968. updateUnitPrice: function(evt) {
  3969. var variant = evt.variant;
  3970.  
  3971. if (variant && variant.unit_price) {
  3972. var price = theme.Currency.formatMoney(variant.unit_price, theme.settings.moneyFormat);
  3973. var base = theme.Currency.getBaseUnit(variant);
  3974.  
  3975. $(selectors.unitWrapper, this.$container)
  3976. .html(price + '/' + base)
  3977. .removeClass('hide').removeClass(classes.visuallyHide);
  3978. } else {
  3979. $(selectors.unitWrapper, this.$container).addClass(classes.visuallyHide);
  3980. }
  3981. },
  3982.  
  3983. updateSku: function(evt) {
  3984. var variant = evt.variant;
  3985. var newSku = '';
  3986.  
  3987. if (variant) {
  3988. if (variant.sku) {
  3989. newSku = variant.sku;
  3990. }
  3991.  
  3992. $(selectors.sku, this.$container).html(newSku);
  3993. }
  3994. },
  3995.  
  3996. updateInventory: function(evt) {
  3997. var variant = evt.variant;
  3998.  
  3999. // If we don't track variant inventory, hide stock
  4000. if (!variant || !variant.inventory_management) {
  4001. this.toggleInventoryQuantity(false);
  4002. this.toggleIncomingInventory(false);
  4003. return;
  4004. }
  4005.  
  4006. if (variant.inventory_management === 'shopify' && window.inventories && window.inventories[this.sectionId]) {
  4007. variantInventoryObject = window.inventories[this.sectionId][variant.id];
  4008. var quantity = variantInventoryObject.quantity;
  4009. var showInventory = true;
  4010. var showIncomingInventory = false;
  4011.  
  4012. if (quantity <= 0 || quantity > theme.settings.inventoryThreshold) {
  4013. showInventory = false;
  4014. }
  4015.  
  4016. this.toggleInventoryQuantity(showInventory, quantity);
  4017.  
  4018. if (!showInventory && variantInventoryObject.incoming) {
  4019. showIncomingInventory = true;
  4020. }
  4021.  
  4022. this.toggleIncomingInventory(showIncomingInventory, variant.available, variantInventoryObject.next_incoming_date);
  4023. }
  4024. },
  4025.  
  4026. toggleInventoryQuantity: function(show, qty) {
  4027. if (!this.settings.inventory) {
  4028. show = false;
  4029. }
  4030.  
  4031. if (show) {
  4032. $(selectors.inventory, this.$container)
  4033. .removeClass('hide')
  4034. .text(theme.strings.stockLabel.replace('[count]', qty));
  4035. } else {
  4036. $(selectors.inventory, this.$container).addClass('hide');
  4037. }
  4038. },
  4039.  
  4040. toggleIncomingInventory: function(show, available, date) {
  4041. if (!this.settings.incomingInventory) {
  4042. show = false;
  4043. }
  4044.  
  4045. if (show) {
  4046. var string = available ?
  4047. theme.strings.willNotShipUntil.replace('[date]', date) :
  4048. theme.strings.willBeInStockAfter.replace('[date]', date);
  4049.  
  4050. if (!date) {
  4051. string = theme.strings.waitingForStock;
  4052. }
  4053.  
  4054. $(selectors.incomingInventory, this.$container)
  4055. .removeClass('hide')
  4056. .text(string);
  4057. } else {
  4058. $(selectors.incomingInventory, this.$container).addClass('hide');
  4059. }
  4060. },
  4061.  
  4062. updateVariantImage: function(evt) {
  4063. var variant = evt.variant;
  4064. var sizedImgUrl = theme.Images.getSizedImageUrl(variant.featured_image.src, this.settings.imageSize);
  4065.  
  4066. var $newImage = $('.product__thumb[data-id="' + variant.featured_image.id + '"]');
  4067. var imageIndex = this._slideIndex($newImage.closest('.product__thumb-item'));
  4068.  
  4069. // No image, bail
  4070. if (typeof imageIndex === 'undefined') {
  4071. return;
  4072. }
  4073.  
  4074. if (!theme.config.bpSmall && this.settings.stackedImages) {
  4075. this.stackedScrollTo(imageIndex);
  4076. } else {
  4077. this.$mainSlider.slick('slickGoTo', imageIndex);
  4078. }
  4079. },
  4080.  
  4081. setCurrentVariantAvailability: function(variant) {
  4082. var valuesToEnable = {
  4083. option1: [],
  4084. option2: [],
  4085. option3: []
  4086. };
  4087.  
  4088. // Disable all options to start
  4089. this.disableVariantGroup($(selectors.formContainer, this.$container).find('.variant-input-wrap'));
  4090.  
  4091. // Combine all available variants
  4092. var availableVariants = this.variantsObject.filter(function(el) {
  4093. if (variant.id === el.id) {
  4094. return false;
  4095. }
  4096.  
  4097. // Option 1
  4098. if (variant.option2 === el.option2 && variant.option3 === el.option3) {
  4099. return true;
  4100. }
  4101.  
  4102. // Option 2
  4103. if (variant.option1 === el.option1 && variant.option3 === el.option3) {
  4104. return true;
  4105. }
  4106.  
  4107. // Option 3
  4108. if (variant.option1 === el.option1 && variant.option2 === el.option2) {
  4109. return true;
  4110. }
  4111. });
  4112.  
  4113.  
  4114. // IE11 can't handle shortform of {variant} so extra step is needed
  4115. var variantObject = {
  4116. variant: variant
  4117. };
  4118.  
  4119. availableVariants = Object.assign({}, variantObject, availableVariants);
  4120.  
  4121. // Loop through each available variant to gather variant values
  4122. for (var property in availableVariants) {
  4123. if (availableVariants.hasOwnProperty(property)) {
  4124. var item = availableVariants[property];
  4125. var option1 = item.option1;
  4126. var option2 = item.option2;
  4127. var option3 = item.option3;
  4128.  
  4129. if (option1) {
  4130. if (valuesToEnable.option1.indexOf(option1) === -1) {
  4131. valuesToEnable.option1.push(option1);
  4132. }
  4133. }
  4134. if (option2) {
  4135. if (valuesToEnable.option2.indexOf(option2) === -1) {
  4136. valuesToEnable.option2.push(option2);
  4137. }
  4138. }
  4139. if (option3) {
  4140. if (valuesToEnable.option3.indexOf(option3) === -1) {
  4141. valuesToEnable.option3.push(option3);
  4142. }
  4143. }
  4144. }
  4145. }
  4146.  
  4147. // Have values to enable, separated by option index
  4148. if (valuesToEnable.option1.length) {
  4149. this.enableVariantOptionByValue(valuesToEnable.option1, 'option1');
  4150. }
  4151. if (valuesToEnable.option2.length) {
  4152. this.enableVariantOptionByValue(valuesToEnable.option2, 'option2');
  4153. }
  4154. if (valuesToEnable.option3.length) {
  4155. this.enableVariantOptionByValue(valuesToEnable.option3, 'option3');
  4156. }
  4157. },
  4158.  
  4159. updateVariantAvailability: function(evt, value, index) {
  4160. if (value && index) {
  4161. var newVal = value;
  4162. var optionIndex = index;
  4163. } else {
  4164. var $el = $(evt.currentTarget);
  4165. var newVal = $el.val() ? $el.val() : evt.currentTarget.value;
  4166. var optionIndex = $el.data('index');
  4167. }
  4168.  
  4169. var variants = this.variantsObject.filter(function(el) {
  4170. return el[optionIndex] === newVal;
  4171. });
  4172.  
  4173. // Disable all buttons/dropdown options that aren't the current index
  4174. $(selectors.formContainer, this.$container).find('.variant-input-wrap').each(function(index, el) {
  4175. var $group = $(el);
  4176. var currentOptionIndex = $group.data('index');
  4177.  
  4178. if (currentOptionIndex !== optionIndex) {
  4179. // Disable all options as a starting point
  4180. this.disableVariantGroup($group);
  4181.  
  4182. // Loop through legit available options and enable
  4183. for (var i = 0; i < variants.length; i++) {
  4184. this.enableVariantOption($group, variants[i][currentOptionIndex]);
  4185. }
  4186. }
  4187. }.bind(this));
  4188. },
  4189.  
  4190. disableVariantGroup: function($group) {
  4191. if (this.settings.variantType === 'dropdown') {
  4192. $group.find('option').prop('disabled', true)
  4193. } else {
  4194. $group.find('input').prop('disabled', true);
  4195. $group.find('label').toggleClass('disabled', true);
  4196. }
  4197. },
  4198.  
  4199. enableVariantOptionByValue: function(array, index) {
  4200. var $group = $(selectors.formContainer, this.$container).find('.variant-input-wrap[data-index="'+ index +'"]');
  4201.  
  4202. for (var i = 0; i < array.length; i++) {
  4203. this.enableVariantOption($group, array[i]);
  4204. }
  4205. },
  4206.  
  4207. enableVariantOption: function($group, value) {
  4208. // Selecting by value so escape it
  4209. value = value.replace(/([ #;&,.+*~\':"!^$[\]()=>|\/@])/g,'\\$1');
  4210.  
  4211. if (this.settings.variantType === 'dropdown') {
  4212. $group.find('option[value="'+ value +'"]').prop('disabled', false);
  4213. } else {
  4214. var $buttonGroup = $group.find('.variant-input[data-value="'+ value +'"]');
  4215. $buttonGroup.find('input').prop('disabled', false);
  4216. $buttonGroup.find('label').toggleClass('disabled', false);
  4217. }
  4218. },
  4219.  
  4220. // Image/thumbnail toggling
  4221. initImageSwitch: function() {
  4222. if (!$(selectors.photoThumbs, this.$container).length) {
  4223. return;
  4224. }
  4225.  
  4226. var self = this;
  4227.  
  4228. $(selectors.photoThumbs, this.$container).on('click', function(evt) {
  4229. evt.preventDefault();
  4230. if (!theme.config.bpSmall && this.settings.stackedImages) {
  4231. var index = $(evt.currentTarget).data('index');
  4232. this.stackedScrollTo(index);
  4233. }
  4234. }.bind(this));
  4235. },
  4236.  
  4237. checkIfVideos: function() {
  4238. var $productVideos = this.$mainSlider.find(selectors.productVideo);
  4239.  
  4240. // Stop if there are 0 videos
  4241. if (!$productVideos.length) {
  4242. return false;
  4243. }
  4244.  
  4245. var videoTypes = [];
  4246.  
  4247. $productVideos.each(function() {
  4248. var type = $(this).data('video-type');
  4249.  
  4250. if (videoTypes.indexOf(type) < 0) {
  4251. videoTypes.push(type);
  4252. }
  4253. });
  4254.  
  4255. // Load YouTube API if not already loaded
  4256. if (videoTypes.indexOf('youtube') > -1) {
  4257. if (!theme.config.youTubeReady) {
  4258. window.loadYouTube();
  4259. $('body').on('youTubeReady' + this.namespace, function() {
  4260. this.loadYoutubeVideos($productVideos);
  4261. }.bind(this));
  4262. } else {
  4263. this.loadYoutubeVideos($productVideos);
  4264. }
  4265. }
  4266.  
  4267. // Load Vimeo API if not already loaded
  4268. if (videoTypes.indexOf('vimeo') > -1) {
  4269. if (!vimeoReady) {
  4270. window.loadVimeo();
  4271. $('body').on('vimeoReady' + this.namespace, function() {
  4272. this.loadVimeoVideos($productVideos);
  4273. }.bind(this))
  4274. } else {
  4275. this.loadVimeoVideos($productVideos);
  4276. }
  4277. }
  4278.  
  4279. // Add mp4 video players
  4280. if (videoTypes.indexOf('mp4') > -1) {
  4281. this.loadMp4Videos($productVideos);
  4282. }
  4283.  
  4284. return videoTypes;
  4285. },
  4286.  
  4287. loadMp4Videos: function($videos) {
  4288. $videos.each(function() {
  4289. var $el = $(this);
  4290. if ($el.data('video-type') != 'mp4') {
  4291. return;
  4292. }
  4293.  
  4294. var id = $el.attr('id');
  4295. var videoId = $el.data('video-id');
  4296.  
  4297. videos[this.id] = {
  4298. type: 'mp4',
  4299. divId: id,
  4300. style: $el.data('video-style')
  4301. };
  4302. });
  4303. },
  4304.  
  4305. loadVimeoVideos: function($videos) {
  4306. $videos.each(function() {
  4307. var $el = $(this);
  4308. if ($el.data('video-type') != 'vimeo') {
  4309. return;
  4310. }
  4311.  
  4312. var id = $el.attr('id');
  4313. var videoId = $el.data('video-id');
  4314.  
  4315. videos[this.id] = {
  4316. type: 'vimeo',
  4317. divId: id,
  4318. id: videoId,
  4319. style: $el.data('video-style'),
  4320. width: $el.data('video-width'),
  4321. height: $el.data('video-height')
  4322. };
  4323. });
  4324.  
  4325. // Create a new player for each Vimeo video
  4326. for (var key in videos) {
  4327. if (videos[key].type != 'vimeo') {
  4328. continue;
  4329. }
  4330.  
  4331. var args = $.extend({}, vimeoVideoOptions, videos[key]);
  4332. vimeoPlayers[key] = new Vimeo.Player(videos[key].divId, args);
  4333. }
  4334.  
  4335. vimeoReady = true;
  4336. },
  4337.  
  4338. autoplayVimeoVideo: function(id) {
  4339. // Do not autoplay on mobile though
  4340. if (!theme.config.bpSmall) {
  4341. this.requestToPlayVimeoVideo(id);
  4342. } else {
  4343. // Set as loaded on mobile so you can see the image
  4344. var $player = $('#' + id);
  4345. setParentAsLoaded($player);
  4346. }
  4347. },
  4348.  
  4349. requestToPlayVimeoVideo: function(id) {
  4350. // The slider may initialize and attempt to play the video before
  4351. // the API is even ready, because it sucks.
  4352.  
  4353. var $player = $('#' + id);
  4354. setParentAsLoading($player);
  4355.  
  4356. if (!vimeoReady) {
  4357. // Wait for the trigger, then play it
  4358. $('body').on('vimeoReady' + this.namespace, function() {
  4359. this.playVimeoVideo(id);
  4360. }.bind(this))
  4361. return;
  4362. }
  4363.  
  4364. this.playVimeoVideo(id);
  4365. },
  4366.  
  4367. playVimeoVideo: function(id) {
  4368. vimeoPlayers[id].play();
  4369.  
  4370. if (videos[id].style === 'muted') {
  4371. vimeoPlayers[id].setVolume(0);
  4372. }
  4373.  
  4374. var $player = $('#' + id);
  4375. setParentAsLoaded($player);
  4376. },
  4377.  
  4378. stopVimeoVideo: function(id) {
  4379. if (!theme.config.vimeoReady) {
  4380. return;
  4381. }
  4382.  
  4383. if (id) {
  4384. vimeoPlayers[id].pause();
  4385. } else {
  4386. for (key in vimeoPlayers) {
  4387. if (typeof vimeoPlayers[key].pause === 'function') {
  4388. vimeoPlayers[key].pause();
  4389. }
  4390. }
  4391. }
  4392. },
  4393.  
  4394. loadYoutubeVideos: function($videos) {
  4395. $videos.each(function() {
  4396. var $el = $(this);
  4397. if ($el.data('video-type') != 'youtube') {
  4398. return;
  4399. }
  4400.  
  4401. var id = $el.attr('id');
  4402. var videoId = $el.data('youtube-id');
  4403.  
  4404. videos[this.id] = {
  4405. type: 'youtube',
  4406. id: id,
  4407. videoId: videoId,
  4408. style: $el.data('video-style'),
  4409. width: $el.data('video-width'),
  4410. height: $el.data('video-height'),
  4411. attemptedToPlay: false
  4412. };
  4413. });
  4414.  
  4415. // Create a player for each YouTube video
  4416. for (var key in videos) {
  4417. if (videos[key].type === 'youtube') {
  4418. if (videos.hasOwnProperty(key)) {
  4419. var args = $.extend({}, youtubeVideoOptions, videos[key]);
  4420.  
  4421. if (args.style === 'muted') {
  4422. // default youtubeVideoOptions, no need to change anything
  4423. } else {
  4424. args.playerVars.controls = 1;
  4425. args.playerVars.autoplay = 0;
  4426. }
  4427.  
  4428. youtubePlayers[key] = new YT.Player(key, args);
  4429. }
  4430. }
  4431. }
  4432.  
  4433. youtubeReady = true;
  4434. },
  4435.  
  4436. requestToPlayYoutubeVideo: function(id, forcePlay) {
  4437. if (!theme.config.youTubeReady) {
  4438. return;
  4439. }
  4440.  
  4441. var $player = $('#' + id);
  4442. setParentAsLoading($player);
  4443.  
  4444. // If video is requested too soon, player might not be ready.
  4445. // Set arbitrary timeout to request it again in a second
  4446. if (typeof youtubePlayers[id].playVideo != 'function') {
  4447. var o = this;
  4448. setTimeout(function() {
  4449. o.playYoutubeVideo(id, forcePlay);
  4450. }, 1000);
  4451. return;
  4452. }
  4453.  
  4454. this.playYoutubeVideo(id, forcePlay);
  4455. },
  4456.  
  4457. playYoutubeVideo: function (id, forcePlay) {
  4458. var $player = $('#' + id);
  4459. setParentAsLoaded($player);
  4460. if (typeof youtubePlayers[id].playVideo === 'function') {
  4461. youtubePlayers[id].playVideo();
  4462. }
  4463.  
  4464. // forcePlay is sent as true from beforeSlideChange so the visibility
  4465. // check isn't fooled by the next slide positioning
  4466. if (!forcePlay) {
  4467. initCheckVisibility(id);
  4468. }
  4469. },
  4470.  
  4471. stopYoutubeVideo: function(id) {
  4472. if (!theme.config.youTubeReady) {
  4473. return;
  4474. }
  4475.  
  4476. if (id && youtubePlayers[id]) {
  4477. if (typeof youtubePlayers[id].pauseVideo === 'function') {
  4478. youtubePlayers[id].pauseVideo();
  4479. }
  4480. $(window).off('scroll.' + id);
  4481. } else {
  4482. for (key in youtubePlayers) {
  4483. if (typeof youtubePlayers[key].pauseVideo === 'function') {
  4484. youtubePlayers[key].pauseVideo();
  4485. $(window).off('scroll.' + key);
  4486. }
  4487. }
  4488. }
  4489. },
  4490.  
  4491. playMp4Video: function(id) {
  4492. var $player = $('#' + id);
  4493. setParentAsLoaded($player);
  4494.  
  4495. $player[0].play();
  4496. },
  4497.  
  4498. stopMp4Video: function(id) {
  4499. if (id) {
  4500. $('#' + id)[0].pause();
  4501. } else {
  4502. // loop through all mp4 videos to stop them
  4503. for (var key in videos) {
  4504. if (videos[key].type === 'mp4') {
  4505. var player = $('#' + videos[key].divId)[0];
  4506. if (typeof player.pause === 'function') {
  4507. player.pause();
  4508. }
  4509. }
  4510. }
  4511. }
  4512. },
  4513.  
  4514. stackedImagesInit: function() {
  4515. $(window).off(this.namespaceImages);
  4516. this.stackedImagePositions();
  4517.  
  4518. if (this.inModal) {
  4519. // Slight delay in modal to accommodate loading videos
  4520. setTimeout(function() {
  4521. this.stackedActive(this.settings.stackedCurrent);
  4522. }.bind(this), 1000);
  4523. } else {
  4524. this.stackedActive(this.settings.stackedCurrent);
  4525. }
  4526.  
  4527.  
  4528. // update image positions on resize
  4529. $(window).on('resize' + this.namespaceImages, $.debounce(200, this.stackedImagePositions.bind(this)));
  4530.  
  4531. // scroll listener to mark active thumbnail
  4532. $(window).on('scroll' + this.namespaceImages, $.throttle(200, function() {
  4533. var goal = window.scrollY;
  4534. var closest = this.settings.stackedImagePositions.reduce(function(prev, curr) {
  4535. return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev);
  4536. });
  4537. var index = this.settings.stackedImagePositions.indexOf(closest)
  4538. if (this.settings.stackedCurrent !== index) {
  4539. this.stackedActive(index);
  4540. }
  4541. }.bind(this)));
  4542. },
  4543.  
  4544. stackedImagePositions: function() {
  4545. var positions = [];
  4546. $(selectors.photo, this.$container).each(function() {
  4547. positions.push(Math.round($(this).offset().top));
  4548. });
  4549. this.settings.stackedImagePositions = positions;
  4550. },
  4551.  
  4552. stackedScrollTo: function(index) {
  4553. // Scroll to top of large image
  4554. var pos = $(selectors.photo, this.$container).eq(index).offset().top;
  4555. $('html, body').animate({
  4556. scrollTop: pos
  4557. }, 400, 'swing');
  4558. },
  4559.  
  4560. stackedActive: function(index) {
  4561. $(selectors.photoThumbItem, this.$container)
  4562. .removeClass(classes.thumbActive)
  4563. .eq(index).addClass(classes.thumbActive);
  4564.  
  4565. if (this.settings.hasVideos) {
  4566. this.stopVideo();
  4567.  
  4568. var $video = $(selectors.photo, this.$container).eq(index).find(selectors.productVideo);
  4569.  
  4570. if ($video.length) {
  4571. this.initVideo($video);
  4572. }
  4573. }
  4574.  
  4575. this.settings.stackedCurrent = index;
  4576. },
  4577.  
  4578. createImageCarousels: function() {
  4579. // Set starting slide (for both sliders)
  4580. var $activeSlide = this.$mainSlider.find('.starting-slide');
  4581. var startIndex = this._slideIndex($activeSlide);
  4582.  
  4583. // Lame way to prevent duplicate event listeners
  4584. this.$mainSlider.off('init');
  4585. this.$mainSlider.off('beforeChange');
  4586. this.$mainSlider.on('init', this.mainSlideInit.bind(this));
  4587. this.$mainSlider.on('beforeChange', this.beforeSlideChange.bind(this));
  4588. this.$thumbSlider.on('init', this.thumbSlideInit.bind(this));
  4589.  
  4590. // Default (mobile) slider settings
  4591. this.mainSliderArgs = {
  4592. infinite: true,
  4593. arrows: false,
  4594. dots: true,
  4595. touchThreshold: 10,
  4596. speed: 300,
  4597. adaptiveHeight: true,
  4598. initialSlide: startIndex
  4599. };
  4600.  
  4601. this.thumbSliderArgs = {
  4602. initialSlide: startIndex
  4603. };
  4604.  
  4605. // Init sliders normally
  4606. var sliderArgs = this.setSliderArgs();
  4607. this.initSliders(sliderArgs);
  4608.  
  4609. // Re-init slider when a breakpoint is hit
  4610. $('body').on('matchSmall matchLarge', function() {
  4611. var sliderArgs = this.setSliderArgs();
  4612. this.initSliders(sliderArgs);
  4613. }.bind(this));
  4614.  
  4615. // Too many thumbnails can cause the AOS calculations to be off
  4616. // so refresh that when the slider is ready
  4617. if (AOS) {
  4618. AOS.refresh();
  4619. }
  4620. },
  4621.  
  4622. initSliders: function(args) {
  4623. this.destroyImageCarousels();
  4624.  
  4625. if (!theme.config.bpSmall && this.settings.stackedImages) {
  4626. this.stackedImagesInit();
  4627. } else {
  4628. this.$mainSlider.not('.slick-initialized').slick(args.main);
  4629. }
  4630.  
  4631. if (!theme.config.bpSmall && !this.settings.stackedImages) {
  4632. if (this.$thumbSlider.length) {
  4633. this.$thumbSlider.not('.slick-initialized').slick(args.thumbs);
  4634. }
  4635. }
  4636. },
  4637.  
  4638. setSliderArgs: function() {
  4639. var args = {};
  4640. var thumbnailsVertical = this.$thumbSlider.data('position') === 'beside' ? true : false;
  4641.  
  4642. if (theme.config.bpSmall) {
  4643. args.main = this.mainSliderArgs;
  4644. args.thumbs = this.thumbSliderArgs;
  4645. } else {
  4646. args.main = $.extend({}, this.mainSliderArgs, {
  4647. asNavFor: '#' + this.$thumbSlider.attr('id'),
  4648. adaptiveHeight: thumbnailsVertical ? false : true,
  4649. dots: false,
  4650. infinite: false,
  4651. fade: true
  4652. });
  4653.  
  4654. args.thumbs = $.extend({}, this.thumbSliderArgs, {
  4655. asNavFor: '#' + this.$mainSlider.attr('id'),
  4656. slidesToShow: thumbnailsVertical ? 3 : 5,
  4657. slidesToScroll: 1,
  4658. arrows: false,
  4659. dots: false,
  4660. vertical: thumbnailsVertical,
  4661. verticalSwiping: thumbnailsVertical,
  4662. focusOnSelect: true,
  4663. infinite: false,
  4664. customHeightMatching: thumbnailsVertical,
  4665. customSlideAdvancement: true
  4666. });
  4667. }
  4668.  
  4669. return args;
  4670. },
  4671.  
  4672. destroyImageCarousels: function() {
  4673. if (this.$mainSlider && this.settings.slickMainInitialized) {
  4674. this.$mainSlider.slick('unslick');
  4675. this.settings.slickMainInitialized = false;
  4676. }
  4677.  
  4678. if (this.$thumbSlider && this.settings.slickThumbInitialized) {
  4679. this.$thumbSlider.slick('unslick');
  4680. this.settings.slickThumbInitialized = false;
  4681. }
  4682.  
  4683. this.settings.slickMainInitialized = false;
  4684. this.settings.slickThumbInitialized = false;
  4685. },
  4686.  
  4687. mainSlideInit: function(event, slick) {
  4688. var $currentSlide = slick.$slider.find(selectors.currentSlide);
  4689. var $video = $currentSlide.find(selectors.productVideo);
  4690.  
  4691. this.settings.slickMainInitialized = true;
  4692.  
  4693. if (!$video.length) {
  4694. return;
  4695. }
  4696.  
  4697. this.initVideo($video);
  4698. },
  4699.  
  4700. thumbSlideInit: function(event, slick) {
  4701. this.settings.slickThumbInitialized = true;
  4702. },
  4703.  
  4704. initVideo: function($video) {
  4705. var videoType = $video.data('video-type');
  4706. var divId = $video.attr('id');
  4707.  
  4708. if (videoType === 'mp4' && videos[divId].style === 'muted') {
  4709. this.playMp4Video(divId);
  4710. }
  4711.  
  4712. if (videoType === 'youtube') {
  4713. if (youtubeReady && videos[divId].style === 'muted') {
  4714. this.requestToPlayYoutubeVideo(divId);
  4715. }
  4716. }
  4717.  
  4718. if (videoType === 'vimeo') {
  4719. if (vimeoReady) {
  4720. this.playOrShowVimeo(divId);
  4721. } else {
  4722. $('body').on('vimeoReady' + this.namespace, function() {
  4723. this.playOrShowVimeo(divId);
  4724. }.bind(this))
  4725. }
  4726. }
  4727.  
  4728. // Hacky way to trigger resetting the slider layout in modals
  4729. if (this.inModal) {
  4730. this.resizeSlides();
  4731. }
  4732. },
  4733.  
  4734. stopVideo: function(id, type) {
  4735. if (!id) {
  4736. this.stopYoutubeVideo();
  4737. this.stopVimeoVideo();
  4738. this.stopMp4Video();
  4739. }
  4740.  
  4741. if (type === 'youtube') {
  4742. this.stopYoutubeVideo(id);
  4743. }
  4744.  
  4745. if (type === 'mp4') {
  4746. this.stopMp4Video(id);
  4747. }
  4748.  
  4749. if (type === 'vimeo') {
  4750. this.stopVimeoVideo(id);
  4751. }
  4752. },
  4753.  
  4754. playOrShowVimeo: function(id) {
  4755. if (videos[id] && videos[id].style === 'muted') {
  4756. this.autoplayVimeoVideo(id);
  4757. } else if (videos[id] && videos[id].style === 'unmuted') {
  4758. setParentAsLoaded($('#' + id));
  4759. }
  4760. },
  4761.  
  4762. getVideoType: function($video) {
  4763. return $video.data('video-type');
  4764. },
  4765.  
  4766. getVideoId: function($video) {
  4767. return $video.attr('id');
  4768. },
  4769.  
  4770. beforeSlideChange: function(event, slick, currentSlide, nextSlide) {
  4771. var $slider = slick.$slider;
  4772. var $currentSlide = $slider.find(selectors.currentSlide);
  4773. var $prevVideo = $currentSlide.find('.product__video');
  4774. var $nextSlide = $slider.find('.slick-slide[data-slick-index="' + nextSlide + '"]');
  4775. var $nextVideo = $nextSlide.find('.product__video');
  4776.  
  4777. // Pause any existing slide video
  4778. if (currentSlide !== nextSlide && $prevVideo.length) {
  4779. var prevVideoType = this.getVideoType($prevVideo);
  4780. var prevVideoId = this.getVideoId($prevVideo);
  4781.  
  4782. if (prevVideoId) {
  4783. this.stopVideo(prevVideoId, prevVideoType);
  4784. }
  4785. }
  4786.  
  4787. // Prep next slide video
  4788. if ($nextVideo.length) {
  4789. var nextVideoType = this.getVideoType($nextVideo);
  4790. var nextVideoId = this.getVideoId($nextVideo);
  4791.  
  4792. // Prep Vimeo with a backup in case the API isn't ready
  4793. if (nextVideoId && nextVideoType === 'vimeo') {
  4794. if (vimeoReady) {
  4795. if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') {
  4796. this.autoplayVimeoVideo(nextVideoId);
  4797. }
  4798. } else {
  4799. $('body').on('vimeoReady' + this.namespace, function() {
  4800. if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') {
  4801. this.autoplayVimeoVideo(nextVideoId);
  4802. }
  4803. }.bind(this))
  4804. }
  4805. }
  4806.  
  4807. // Prep YouTube with a backup in case API isn't ready
  4808. if (nextVideoId && nextVideoType === 'youtube') {
  4809. if (youtubeReady) {
  4810. if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') {
  4811. this.requestToPlayYoutubeVideo(nextVideoId, true);
  4812. }
  4813. } else {
  4814. $('body').on('youTubeReady' + this.namespace, function() {
  4815. if (videos[nextVideoId] && videos[nextVideoId].style === 'muted') {
  4816. this.requestToPlayYoutubeVideo(nextVideoId, true);
  4817. }
  4818. }.bind(this))
  4819. }
  4820. }
  4821.  
  4822. // Autoplay muted MP4 videos
  4823. if (nextVideoId && videos[nextVideoId] && videos[nextVideoId].style === 'muted') {
  4824. if (nextVideoType === 'mp4') {
  4825. this.playMp4Video(nextVideoId);
  4826. }
  4827. }
  4828.  
  4829. // Set unmuted videos to loaded state
  4830. if (nextVideoId && videos[nextVideoId] && videos[nextVideoId].style != 'muted') {
  4831. setParentAsLoaded($('#' + nextVideoId));
  4832. }
  4833. }
  4834. },
  4835.  
  4836. resizeSlides: function() {
  4837. if (!this.settings.hasMultipleImages) {
  4838. return;
  4839. }
  4840.  
  4841. // Necessary to make slider visible again
  4842. $(window).trigger('resize.slick');
  4843. setTimeout(function() {
  4844. if (this.$mainSlider && this.settings.slickMainInitialized) {
  4845. this.$mainSlider.slick('setPosition');
  4846. }
  4847. if (this.$thumbSlider && this.settings.slickThumbInitialized) {
  4848. this.$thumbSlider.slick('setPosition');
  4849. }
  4850. }.bind(this), 500); // same timing as modal open transition
  4851. },
  4852.  
  4853. _slideIndex: function($el) {
  4854. return $el.data('index');
  4855. },
  4856.  
  4857. initAjaxProductForm: function() {
  4858. if (theme.settings.cartType === 'drawer' || theme.settings.cartType === 'sticky') {
  4859. new theme.AjaxProduct($(selectors.formContainer, this.$container));
  4860. }
  4861. },
  4862.  
  4863. openModalProduct: function() {
  4864. var initialized = false;
  4865. if (!this.settings.modalInit) {
  4866. var url = this.$formHolder.data('url');
  4867. var template = this.$formHolder.data('template');
  4868.  
  4869. // If not template, product uses default product template
  4870. // which has sections. Ajax view is a slimmed down version to
  4871. // load only essentials
  4872. if (!template) {
  4873. url = url + '?view=ajax';
  4874. }
  4875.  
  4876. $.get(url, function(data) {
  4877. var $template = $(data);
  4878. var $newForm = $template.find('#AddToCartForm-' + this.sectionId);
  4879. this.replaceModalFormHolder(this.$formHolder, $newForm);
  4880.  
  4881. var $sectionDiv = $template.find('#ProductSections-' + this.sectionId);
  4882. if ($sectionDiv.length) {
  4883. this.loadProductSections($sectionDiv);
  4884. }
  4885.  
  4886. var $relatedDiv = $template.find('#Recommendations-' + this.sectionId);
  4887. if ($relatedDiv.length) {
  4888. this.loadRelatedProducts($relatedDiv);
  4889. }
  4890.  
  4891. var $socialDiv = $template.find('.index-section.instagram-section');
  4892. if ($socialDiv.length) {
  4893. this.loadSocialSection($socialDiv);
  4894. }
  4895.  
  4896. if (window.SPR) {
  4897. SPR.initDomEls();SPR.loadBadges();
  4898. }
  4899.  
  4900. sections.loadSubSections(this.$modal);
  4901.  
  4902. document.dispatchEvent(new CustomEvent('quickview:loaded', {
  4903. detail: {
  4904. productId: this.sectionId
  4905. }
  4906. }));
  4907. }.bind(this));
  4908.  
  4909. this.preImageSetup();
  4910. this.loadModalContent();
  4911. this.imageSetup(false);
  4912. this.settings.modalInit = true;
  4913. } else {
  4914. initialized = true;
  4915. if (!theme.config.bpSmall && this.settings.stackedImages) {
  4916. this.stackedActive(0);
  4917. }
  4918. }
  4919.  
  4920. document.dispatchEvent(new CustomEvent('quickview:open', {
  4921. detail: {
  4922. initialized: initialized,
  4923. productId: this.sectionId
  4924. }
  4925. }));
  4926.  
  4927. this.resizeSlides();
  4928. },
  4929.  
  4930. closeModalProduct: function() {
  4931. this.stopVideo();
  4932. $('body').off(this.namespace);
  4933. $(window).off(this.namespace);
  4934. },
  4935.  
  4936. replaceModalFormHolder: function($holder, $form) {
  4937. $holder.replaceWith($form);
  4938. this.formSetup();
  4939. if (Shopify.PaymentButton) {
  4940. Shopify.PaymentButton.init();
  4941. }
  4942. },
  4943.  
  4944. loadProductSections: function($content) {
  4945. $('#ProductSectionsHolder-' + this.sectionId).replaceWith($content);
  4946. },
  4947.  
  4948. loadRelatedProducts: function($content) {
  4949. // Remove any quick view modals as they cause conflicts.
  4950. // These are not output with product.ajax templates,
  4951. // but are in our custom product.sections ones and any custom ones
  4952. // developers create.
  4953. $content.find('.screen-layer--product').remove();
  4954.  
  4955. $('#ProductRelatedHolder-' + this.sectionId).replaceWith($content);
  4956. },
  4957.  
  4958. loadSocialSection: function($content) {
  4959. $('#SocialSectionHolder-' + this.sectionId).replaceWith($content);
  4960. },
  4961.  
  4962. loadModalContent: function() {
  4963. // Load videos if they exist
  4964. var videoTypes = this.checkIfVideos();
  4965.  
  4966. // Lazyload mp4 videos similar to images
  4967. if (videoTypes && videoTypes.indexOf('mp4') > -1) {
  4968. this.$modal.find('.product__video[data-video-type="mp4"]').each(function(i, video) {
  4969. var $el = $(video);
  4970. var src = $el.data('video-src');
  4971. var source = document.createElement('source');
  4972. source.setAttribute('src', src);
  4973. $el.append(source);
  4974. }.bind(this));
  4975. }
  4976. },
  4977.  
  4978. onUnload: function() {
  4979. this.$container.off(this.namespace);
  4980. $('body').off(this.namespace);
  4981. $(window).off(this.namespace).off(this.namespaceImages);;
  4982. this.destroyImageCarousels();
  4983.  
  4984. if (AOS) {
  4985. AOS.refresh();
  4986. }
  4987. }
  4988. });
  4989.  
  4990. return Product;
  4991. })();
  4992.  
  4993. theme.Recommendations = (function() {
  4994.  
  4995. function Recommendations(container) {
  4996. var $container = this.$container = $(container);
  4997. var sectionId = this.sectionId = $container.attr('data-section-id');
  4998.  
  4999. this.selectors = {
  5000. recommendations: '#Recommendations-' + sectionId,
  5001. placeholder: '.product-recommendations-placeholder',
  5002. sectionClass: ' .product-recommendations',
  5003. productResults: '.grid-product'
  5004. };
  5005.  
  5006. this.init();
  5007. }
  5008.  
  5009. Recommendations.prototype = $.extend({}, Recommendations.prototype, {
  5010. init: function() {
  5011. var $section = $(this.selectors.recommendations);
  5012.  
  5013. if (!$section.length || $section.data('enable') === false) {
  5014. return;
  5015. }
  5016.  
  5017. var $placeholder = $section.find(this.selectors.placeholder);
  5018. var id = $section.data('product-id');
  5019. var limit = $section.data('limit');
  5020.  
  5021. var url = '/recommendations/products?section_id=product-recommendations&limit='+ limit +'&product_id=' + id;
  5022.  
  5023. $placeholder.load(url + this.selectors.sectionClass, function(data) {
  5024. theme.reinitProductGridItem($section);
  5025.  
  5026. // If no results, hide the entire section
  5027. if ($(data).find(this.selectors.sectionClass).find(this.selectors.productResults).length === 0) {
  5028. $section.addClass('hide');
  5029. }
  5030. }.bind(this));
  5031. }
  5032. });
  5033.  
  5034. return Recommendations;
  5035. })();
  5036.  
  5037. // Handles multiple section interactions:
  5038. // - Featured collection slider
  5039. // - Featured collection grid (hover product sliders only)
  5040. // - Related products
  5041. // - Social reviews
  5042. //
  5043. // Options:
  5044. // - scrollable: overflow div with arrows
  5045. // - infinite pagination: only in slider format
  5046.  
  5047. theme.FeaturedCollection = (function() {
  5048. var selectors = {
  5049. scrollWrap: '[data-pagination-wrapper]',
  5050. scrollAnimation: '[data-aos="overflow__animation"]',
  5051. productContainer: '[data-product-container]',
  5052. collectionProductContainer: '[data-collection-container]',
  5053. product: '[data-product-grid]',
  5054. arrows: '[data-arrow]'
  5055. };
  5056.  
  5057. var classes = {
  5058. loading: 'collection-loading',
  5059. arrowLeft: 'overflow-scroller__arrow--left',
  5060. disableScrollLeft: 'overflow-scroller--disable-left',
  5061. disableScrollRight: 'overflow-scroller--disable-right'
  5062. };
  5063.  
  5064. var config = {
  5065. instagramLoaded: false
  5066. };
  5067.  
  5068. function FeaturedCollection(container) {
  5069. this.$container = $(container);
  5070. this.sectionId = this.$container.attr('data-section-id');
  5071. this.$scrollWrap = $(selectors.scrollWrap, this.$container);
  5072. this.$scrollArrows = $(selectors.arrows, this.$container);
  5073. this.namespace = '.featured-collection-' + this.sectionId;
  5074.  
  5075. this.options = {
  5076. scrollable: this.$container.data('scrollable'),
  5077. paginate: this.$container.data('paginate'),
  5078. instagram: this.$container.data('instagram')
  5079. };
  5080.  
  5081. var paginateBy = this.$container.data('paginate-by');
  5082. var productCount = this.$container.data('collection-count');
  5083.  
  5084. this.settings = {
  5085. url: this.$container.data('collection-url'),
  5086. page: 1,
  5087. pageCount: this.options.paginate ? Math.ceil(productCount / paginateBy) : 0,
  5088. itemsToScroll: 3
  5089. };
  5090.  
  5091. this.state = {
  5092. isInit: false,
  5093. loading: false,
  5094. scrollerEnabled: false,
  5095. loadedAllProducts: false,
  5096. scrollable: this.options.scrollable,
  5097. scrollInterval: null,
  5098. scrollSpeed: 3 // smaller is faster
  5099. };
  5100.  
  5101. this.sizing = {
  5102. scroller: 0,
  5103. itemWidth: 0
  5104. };
  5105.  
  5106. slate.utils.promiseStylesheet().then(function() {
  5107. this.checkVisibility();
  5108. $(window).on('scroll' + this.namespace, $.debounce(200, this.checkVisibility.bind(this)));
  5109. }.bind(this));
  5110. }
  5111.  
  5112. FeaturedCollection.prototype = $.extend({}, FeaturedCollection.prototype, {
  5113. checkVisibility: function() {
  5114. if (this.state.isInit) {
  5115. // If a value is 0, we need to recalculate starting points
  5116. if (this.sizing.scrollSize === 0) {
  5117. this.$scrollWrap.trigger('scroll' + this.namespace);
  5118. }
  5119. $(window).off('scroll' + this.namespace);
  5120. return;
  5121. }
  5122.  
  5123. var visibleThreshold = this.options.instagram ? 500 : 0;
  5124.  
  5125. if (theme.isElementVisible(this.$container, visibleThreshold)) {
  5126. if (this.options.instagram) {
  5127. this.initInstagram();
  5128. }
  5129. this.init();
  5130. this.state.isInit = true;
  5131. }
  5132. },
  5133.  
  5134. init: function() {
  5135. new theme.HoverProductGrid(this.$container);
  5136.  
  5137. if (!this.state.scrollable) {
  5138. return;
  5139. }
  5140.  
  5141. this.sizing = this.getScrollWidths();
  5142.  
  5143. $(window).on('resize' + this.namespace, $.debounce(200, this.handleResize.bind(this)));
  5144.  
  5145. this.toggleScrollListener(this.state.scrollable);
  5146. this.arrowListeners(this.state.scrollable);
  5147. },
  5148.  
  5149. reInit: function() {
  5150. new theme.HoverProductGrid(this.$container);
  5151.  
  5152. if (this.state.scrollable) {
  5153. this.sizing = this.getScrollWidths();
  5154. this.toggleScrollListener(this.state.scrollable);
  5155. }
  5156.  
  5157. theme.reinitProductGridItem();
  5158. },
  5159.  
  5160. initInstagram: function() {
  5161. $('.instagram-image', this.$container).each(function(i, el) {
  5162. if ($(el).is('[data-url]')){
  5163. this.getInstagramImage($(el));
  5164. }
  5165. }.bind(this));
  5166. },
  5167.  
  5168. getInstagramImage: function($el) {
  5169. var instagramLink = $el.attr('data-url')
  5170. var regex = /(instagram.com|instagr.am)\/p\/([^\/\s\?\#]+)/;
  5171. if ( regex.test(instagramLink) ){
  5172. var found = regex.exec(instagramLink);
  5173. var shortcode = found.slice(-1).pop();
  5174. var oembed_url = 'https://www.instagram.com/p/' + encodeURIComponent( shortcode ) + '/media/?size=l';
  5175. var request = new XMLHttpRequest();
  5176. request.open('GET', oembed_url, true);
  5177. request.onload = function() {
  5178. if (request.status >= 200 && request.status < 400) {
  5179. $el.css('background-image', 'url(' + request.responseURL + ')' );
  5180.  
  5181. // Remove min-height now that some content is loaded
  5182. if (!config.instagramLoaded) {
  5183. $('.instagram-section__grid').css('min-height', '0');
  5184. config.instagramLoaded = true;
  5185. }
  5186. } else {
  5187. console.warn('Instagram request failed');
  5188. }
  5189. };
  5190. request.onerror = function() {
  5191. console.warn('Instagram request failed');
  5192. };
  5193. request.send();
  5194. } else {
  5195. $el
  5196. .html('Invalid Instagram link. Use this format:<small>https://instagram.com/p/BeQb_V9jyDr</small>')
  5197. .addClass('instagram-image--error');
  5198. }
  5199. },
  5200.  
  5201. loadingState: function(loading) {
  5202. this.state.loading = loading;
  5203. this.$container.toggleClass(classes.loading, loading);
  5204. },
  5205.  
  5206. getScrollWidths: function() {
  5207. var container = this.$scrollWrap.width();
  5208. var scroller = this.$scrollWrap[0].scrollWidth;
  5209. var itemWidth = this.$scrollWrap.find('.grid__item').first().outerWidth();
  5210.  
  5211. // First time this runs there is a 200px CSS animation that JS doesn't
  5212. // take into account, so manually subtract from the scroller width
  5213. if (!this.state.isInit) {
  5214. scroller = scroller - 200;
  5215. }
  5216.  
  5217. if (scroller <= container) {
  5218. this.disableArrow(null, true);
  5219. }
  5220.  
  5221. return {
  5222. scroller: scroller,
  5223. scrollSize: scroller - container,
  5224. itemWidth: itemWidth
  5225. };
  5226. },
  5227.  
  5228. handleResize: function() {
  5229. if (this.state.scrollable) {
  5230. this.sizing = this.getScrollWidths();
  5231. }
  5232. this.toggleScrollListener(this.state.scrollable);
  5233. this.arrowListeners(this.state.scrollable);
  5234. },
  5235.  
  5236. toggleScrollListener: function(enable) {
  5237. if (enable) {
  5238. if (this.state.scrollerEnabled) { return; }
  5239. this.$scrollWrap.on('scroll' + this.namespace, $.throttle(250, this.scrollCheck.bind(this)));
  5240. this.state.scrollerEnabled = true;
  5241. } else {
  5242. this.$scrollWrap.off('scroll' + this.namespace);
  5243. this.state.scrollerEnabled = false;
  5244. }
  5245. },
  5246.  
  5247. scrollCheck: function(evt) {
  5248. if (this.state.loading) {
  5249. this.toggleScrollListener(false);
  5250. return;
  5251. }
  5252.  
  5253. // If a value is 0, we need to recalculate starting points
  5254. if (this.sizing.scrollSize === 0) {
  5255. this.sizing = this.getScrollWidths();
  5256. }
  5257.  
  5258. var scrollLeft = evt.currentTarget.scrollLeft ? evt.currentTarget.scrollLeft : 0;
  5259. var percent = Math.floor(scrollLeft / this.sizing.scrollSize * 100);
  5260. var fromEnd = this.sizing.scrollSize - scrollLeft;
  5261.  
  5262. if (this.options.paginate) {
  5263. if (!this.state.loadedAllProducts && percent > 50) {
  5264. this.getNewProducts();
  5265. }
  5266. }
  5267.  
  5268. if (!percent) {
  5269. percent = 0;
  5270. }
  5271.  
  5272. this.disableArrow(percent);
  5273. },
  5274.  
  5275. arrowListeners: function(enable) {
  5276. if (enable) {
  5277. this.$scrollArrows
  5278. .removeClass('hide')
  5279. .off(this.namespace)
  5280. .on('click' + this.namespace, this.arrowScroll.bind(this));
  5281. } else {
  5282. this.$scrollArrows
  5283. .addClass('hide')
  5284. .off(this.namespace);
  5285. }
  5286. },
  5287.  
  5288. arrowScroll: function(evt) {
  5289. var direction = $(evt.currentTarget).hasClass(classes.arrowLeft) ? 'left' : 'right';
  5290. var iteration = theme.config.bpSmall ? 1 : 2;
  5291.  
  5292. if (evt.type === 'mouseenter') {
  5293. this.state.scrollInterval = setInterval(function(){
  5294. var currentPos = this.$scrollWrap.scrollLeft();
  5295. var newPos = direction === 'left' ? (currentPos - iteration) : (currentPos + iteration);
  5296. this.$scrollWrap.scrollLeft(newPos);
  5297. }.bind(this), this.state.scrollSpeed);
  5298. } else if (evt.type === 'mouseleave') {
  5299. clearInterval(this.state.scrollInterval);
  5300. } else if (evt.type === 'click') {
  5301. clearInterval(this.state.scrollInterval);
  5302.  
  5303. var currentPos = this.$scrollWrap.scrollLeft();
  5304. var scrollAmount = this.sizing.itemWidth * this.settings.itemsToScroll;
  5305. var newPos = direction === 'left' ? (currentPos - scrollAmount) : (currentPos + scrollAmount);
  5306.  
  5307. this.$scrollWrap.stop().animate({
  5308. scrollLeft: newPos
  5309. }, 400, 'swing');
  5310. }
  5311.  
  5312. if (newPos <= 0) {
  5313. this.disableArrow(newPos);
  5314. }
  5315. },
  5316.  
  5317. disableArrow: function(pos, all) {
  5318. this.$scrollArrows
  5319. .removeClass(classes.disableScrollRight)
  5320. .removeClass(classes.disableScrollLeft);
  5321.  
  5322. if (all) {
  5323. this.$scrollArrows
  5324. .addClass(classes.disableScrollRight)
  5325. .addClass(classes.disableScrollLeft);
  5326. return;
  5327. }
  5328.  
  5329. // Max left scroll
  5330. if (pos <= 0) {
  5331. this.$scrollArrows.addClass(classes.disableScrollLeft);
  5332. return;
  5333. }
  5334.  
  5335. // Max right scroll
  5336. if (pos >= 96) {
  5337. this.$scrollArrows.addClass(classes.disableScrollRight);
  5338. return;
  5339. }
  5340. },
  5341.  
  5342. getNewProducts: function() {
  5343. this.loadingState(true);
  5344. var newPage = this.settings.page + 1;
  5345.  
  5346. // No more pages, disable features
  5347. if (newPage > this.settings.pageCount) {
  5348. this.loadingState(false);
  5349. this.state.loadedAllProducts = true;
  5350. return;
  5351. }
  5352.  
  5353. var newUrl = this.settings.url + '?page=' + (newPage);
  5354.  
  5355. $.get(newUrl, function(data) {
  5356. var $template = $(data);
  5357. var $newProducts = $template.find(selectors.collectionProductContainer + ' .grid-product');
  5358.  
  5359. $(selectors.productContainer, this.$container).append($newProducts);
  5360. this.ajaxSuccess();
  5361. }.bind(this));
  5362. },
  5363.  
  5364. ajaxSuccess: function() {
  5365. this.loadingState(false);
  5366. this.settings.page = this.settings.page + 1;
  5367. this.reInit();
  5368. },
  5369.  
  5370. forceReload: function() {
  5371. this.onUnload();
  5372. this.init();
  5373. },
  5374.  
  5375. // Only runs in the editor while a user is activating.
  5376. // Rearranges quick shop modals to fix potentially broken layout
  5377. onLoad: function() {
  5378. theme.QuickShopScreens.reInit(this.$container);
  5379. },
  5380.  
  5381. onUnload: function() {
  5382. $(window).off(this.namespace).trigger('resize');
  5383. this.$scrollWrap.off(this.namespace);
  5384. theme.QuickShopScreens.unload(this.$container);
  5385. }
  5386.  
  5387. });
  5388.  
  5389. return FeaturedCollection;
  5390. })();
  5391.  
  5392. theme.Collection = (function() {
  5393. var isAnimating = false;
  5394.  
  5395. var classes = {
  5396. tags: '.tags',
  5397. activeTag: 'tag--active'
  5398. };
  5399.  
  5400. function Collection(container) {
  5401. this.container = container;
  5402. this.sectionId = $(container).attr('data-section-id');
  5403. this.namespace = '.collection-' + this.sectionId;
  5404.  
  5405. var hasHeroImage = $(container).find('.collection-hero').length;
  5406.  
  5407. if (hasHeroImage) {
  5408. this.checkIfNeedReload();
  5409. } else if (theme.settings.overlayHeader) {
  5410. theme.headerNav.disableOverlayHeader();
  5411. }
  5412.  
  5413. // Ajax pagination
  5414. $(window).on('popstate', function(state) {
  5415. if (state) {
  5416.  
  5417. // Bail if it's a hash link
  5418. if(location.href.indexOf(location.pathname) >= 0) {
  5419. return true;
  5420. }
  5421.  
  5422. this.getNewCollectionContent(location.href);
  5423. }
  5424. }.bind(this));
  5425.  
  5426. this.init();
  5427. }
  5428.  
  5429. Collection.prototype = $.extend({}, Collection.prototype, {
  5430. init: function() {
  5431. // init is called on load and when tags are selected
  5432. this.$container = $(this.container);
  5433. this.sectionId = this.$container.attr('data-section-id');
  5434.  
  5435. new theme.HoverProductGrid(this.$container);
  5436. this.sortBy();
  5437. this.sortTags();
  5438. this.initTagAjax();
  5439. },
  5440.  
  5441. initTagAjax: function() {
  5442. this.$container.on('click' + this.namespace, '.tags a', function(evt) {
  5443. var $el = $(evt.currentTarget);
  5444. if ($el.hasClass('no-ajax')) {
  5445. return;
  5446. }
  5447.  
  5448. evt.preventDefault();
  5449.  
  5450. if (isAnimating) {
  5451. return;
  5452. }
  5453.  
  5454. isAnimating = true;
  5455.  
  5456. var newUrl = $el.attr('href');
  5457. $(classes.tags).find('.' + classes.activeTag).removeClass(classes.activeTag);
  5458. $el.parent().addClass(classes.activeTag);
  5459. history.pushState({}, '', newUrl);
  5460. $('.grid-product').addClass('unload');
  5461. this.getNewCollectionContent(newUrl);
  5462. }.bind(this));
  5463. },
  5464.  
  5465. getNewCollectionContent: function(url) {
  5466. url = url + '?view=ajax';
  5467. $('#CollectionAjaxResult').load(url + ' #CollectionAjaxContent', function() {
  5468. isAnimating = false;
  5469. this.reInit();
  5470. }.bind(this));
  5471. },
  5472.  
  5473. sortBy: function() {
  5474. var $sortBy = $('#SortBy');
  5475.  
  5476. if (!$sortBy.length) {
  5477. return;
  5478. }
  5479.  
  5480. $sortBy.on('change', function() {
  5481. location.href = '?sort_by=' + $(this).val();
  5482. });
  5483. },
  5484.  
  5485. sortTags: function() {
  5486. var $sortTags = $('#SortTags');
  5487.  
  5488. if (!$sortTags.length) {
  5489. return;
  5490. }
  5491.  
  5492. $sortTags.on('change', function() {
  5493. location.href = $(this).val();
  5494. });
  5495. },
  5496.  
  5497. // A liquid variable in the header needs a full page refresh
  5498. // if the collection header hero image setting is enabled
  5499. // and the header is set to sticky. Only necessary in the editor.
  5500. checkIfNeedReload: function() {
  5501. if (!Shopify.designMode) {
  5502. return;
  5503. }
  5504.  
  5505. if (!theme.settings.overlayHeader) {
  5506. return;
  5507. }
  5508.  
  5509. if (!$('.header-wrapper').hasClass('header-wrapper--overlay')) {
  5510. location.reload();
  5511. }
  5512. },
  5513.  
  5514. reInit: function() {
  5515. sections.reinitSection('collection-template');
  5516.  
  5517. theme.reinitProductGridItem();
  5518. },
  5519.  
  5520. forceReload: function() {
  5521. this.onUnload();
  5522. this.init();
  5523. },
  5524.  
  5525. onUnload: function() {
  5526. $(window).off(this.namespace);
  5527. this.$container.off(this.namespace);
  5528. }
  5529.  
  5530. });
  5531.  
  5532. return Collection;
  5533. })();
  5534.  
  5535. theme.HeaderSection = (function() {
  5536.  
  5537. function Header(container) {
  5538. var $container = this.$container = $(container);
  5539. var sectionId = this.sectionId = $container.attr('data-section-id');
  5540.  
  5541. this.initDrawers();
  5542. theme.headerNav.init();
  5543. theme.slideNav.init();
  5544.  
  5545. // Reload any slideshow when the header is reloaded to make sure the
  5546. // sticky header works as expected (it can be anywhere in the sections.instance array)
  5547. sections.reinitSection('slideshow-section');
  5548. }
  5549.  
  5550. Header.prototype = $.extend({}, Header.prototype, {
  5551. initDrawers: function() {
  5552. if ($(document.body).hasClass('template-cart')) {
  5553. new theme.AjaxCart('CartPage');
  5554. } else if (theme.settings.cartType === 'drawer') {
  5555. new theme.AjaxCart('CartDrawer');
  5556. }
  5557. },
  5558.  
  5559. onUnload: function() {
  5560. theme.headerNav.unload();
  5561. theme.slideNav.unload();
  5562. }
  5563. });
  5564.  
  5565. return Header;
  5566.  
  5567. })();
  5568.  
  5569. theme.FeaturedContentSection = (function() {
  5570.  
  5571. function FeaturedContent() {
  5572. $('.rte').find('a:not(:has(img))').addClass('text-link');
  5573. }
  5574.  
  5575. return FeaturedContent;
  5576. })();
  5577.  
  5578. theme.slideshows = {};
  5579.  
  5580. theme.SlideshowSection = (function() {
  5581.  
  5582. function SlideshowSection(container) {
  5583. var $container = this.$container = $(container);
  5584. var $section = $container.parent();
  5585. var sectionId = $container.attr('data-section-id');
  5586. var slideshow = this.slideshow = '#Slideshow-' + sectionId;
  5587.  
  5588. var $imageContainer = $(container).find('.hero');
  5589. if ($imageContainer.length) {
  5590. theme.loadImageSection($imageContainer);
  5591. }
  5592.  
  5593. this.init();
  5594. }
  5595.  
  5596. SlideshowSection.prototype = $.extend({}, SlideshowSection.prototype, {
  5597. init: function() {
  5598. var args = {
  5599. arrows: $(this.slideshow).data('arrows'),
  5600. dots: $(this.slideshow).data('dots'),
  5601. pauseOnHover: true
  5602. };
  5603.  
  5604. theme.slideshows[this.slideshow] = new theme.Slideshow(this.slideshow, args);
  5605. },
  5606.  
  5607. forceReload: function() {
  5608. this.onUnload();
  5609. this.init();
  5610. },
  5611.  
  5612. onUnload: function() {
  5613. theme.slideshows[this.slideshow].destroy();
  5614. delete theme.slideshows[this.slideshow];
  5615. },
  5616.  
  5617. onSelect: function() {
  5618. $(this.slideshow).slick('slickPause');
  5619. },
  5620.  
  5621. onDeselect: function() {
  5622. $(this.slideshow).slick('slickPlay');
  5623. },
  5624.  
  5625. onBlockSelect: function(evt) {
  5626. var $slideshow = $(this.slideshow);
  5627.  
  5628. // Ignore the cloned version
  5629. var $slide = $('.slideshow__slide--' + evt.detail.blockId + ':not(.slick-cloned)');
  5630. var slideIndex = $slide.data('slick-index');
  5631.  
  5632. // Go to selected slide, pause autoplay
  5633. $slideshow.slick('slickGoTo', slideIndex).slick('slickPause');
  5634. },
  5635.  
  5636. onBlockDeselect: function() {
  5637. $(this.slideshow).slick('slickPlay');
  5638. }
  5639. });
  5640.  
  5641. return SlideshowSection;
  5642. })();
  5643.  
  5644. theme.HeroAnimated = (function() {
  5645.  
  5646. var classes = {
  5647. active: 'animated__slide--active',
  5648. inactive: 'animated__slide--inactive'
  5649. }
  5650.  
  5651. function HeroAnimated(container) {
  5652. var $container = this.$container = $(container);
  5653. var $section = $container.parent();
  5654. var sectionId = $container.attr('data-section-id');
  5655. var imageCount = $container.data('count');
  5656. var namespace = '.hero-animated-' + sectionId;
  5657.  
  5658. var $imageContainer = $(container).find('.hero');
  5659. if ($imageContainer.length) {
  5660. theme.loadImageSection($imageContainer);
  5661. }
  5662. this.$allImages = $container.find('.animated__slide');
  5663.  
  5664. this.state = {
  5665. active: false,
  5666. activeIndex: 0
  5667. };
  5668.  
  5669. if (imageCount === 1) {
  5670. this.setFades(true);
  5671. return;
  5672. }
  5673.  
  5674. this.interval;
  5675. this.intervalSpeed = $container.data('interval');
  5676. this.maxIndex = imageCount - 1;
  5677.  
  5678. slate.utils.promiseStylesheet().then(function() {
  5679. this.checkVisibility();
  5680. $(window).on('scroll' + namespace, $.throttle(300, this.checkVisibility.bind(this)));
  5681. }.bind(this));
  5682. }
  5683.  
  5684. HeroAnimated.prototype = $.extend({}, HeroAnimated.prototype, {
  5685. checkVisibility: function() {
  5686. if (!theme.isElementVisible(this.$container)) {
  5687. this.state.active = false;
  5688. clearInterval(this.interval);
  5689. return;
  5690. }
  5691.  
  5692. if (this.state.active) {
  5693. return;
  5694. }
  5695.  
  5696. this.initInterval();
  5697. },
  5698.  
  5699. initInterval: function() {
  5700. this.state.active = true;
  5701.  
  5702. this.setFades(true);
  5703. this.interval = setInterval(function() {
  5704. this.setFades();
  5705. }.bind(this), this.intervalSpeed);
  5706. },
  5707.  
  5708. setFades: function(first) {
  5709. // Get next image index
  5710. var nextIndex = this.state.activeIndex === this.maxIndex ? 0 : this.state.activeIndex + 1;
  5711.  
  5712. if (first) {
  5713. nextIndex = this.state.activeIndex;
  5714. }
  5715.  
  5716. // Unset existing image
  5717. if (!first) {
  5718. this.$allImages.eq(this.state.activeIndex)
  5719. .removeClass(classes.active)
  5720. .addClass(classes.inactive);
  5721. }
  5722.  
  5723. // Set next image as active
  5724. this.$allImages.eq(nextIndex)
  5725. .removeClass(classes.inactive)
  5726. .addClass(classes.active);
  5727.  
  5728. this.state.activeIndex = nextIndex;
  5729. },
  5730.  
  5731. onUnload: function() {
  5732. clearInterval(this.interval);
  5733. }
  5734. });
  5735.  
  5736. return HeroAnimated;
  5737. })();
  5738.  
  5739. theme.VideoSection = (function() {
  5740. var youtubeReady;
  5741. var videos = [];
  5742. var youtubePlayers = [];
  5743. var youtubeVideoOptions = {
  5744. width: 1280,
  5745. height: 720,
  5746. playerVars: {
  5747. autohide: 0,
  5748. branding: 0,
  5749. cc_load_policy: 0,
  5750. controls: 0,
  5751. fs: 0,
  5752. iv_load_policy: 3,
  5753. modestbranding: 1,
  5754. playsinline: 1,
  5755. quality: 'hd720',
  5756. rel: 0,
  5757. showinfo: 0,
  5758. wmode: 'opaque'
  5759. },
  5760. events: {
  5761. onReady: onVideoPlayerReady,
  5762. onStateChange: onVideoStateChange
  5763. }
  5764. };
  5765.  
  5766. var vimeoReady = false;
  5767. var vimeoVideoOptions = {
  5768. byline: false,
  5769. title: false,
  5770. portrait: false,
  5771. loop: true
  5772. };
  5773.  
  5774. var selectors = {
  5775. videoParent: '.video-parent-section'
  5776. };
  5777.  
  5778. var classes = {
  5779. loading: 'loading',
  5780. loaded: 'loaded',
  5781. interactable: 'video-interactable'
  5782. };
  5783.  
  5784. function videoSection(container) {
  5785. var $container = this.$container = $(container);
  5786. var sectionId = this.sectionId = $container.attr('data-section-id');
  5787. var youtubePlayerId = this.youtubePlayerId = 'YouTubeVideo-' + this.sectionId;
  5788. this.namespace = '.' + youtubePlayerId;
  5789. var vimeoPlayerId = this.vimeoPlayerId = 'Vimeo-' + this.sectionId;
  5790. var $vimeoTrigger = this.$vimeoTrigger = $('#VimeoTrigger-' + this.sectionId);
  5791. var mp4Video = 'Mp4Video-' + this.sectionId;
  5792.  
  5793. var $youtubeDiv = $('#' + youtubePlayerId);
  5794. var $vimeoDiv = $('#' + vimeoPlayerId);
  5795. var $mp4Div = $('#' + mp4Video);
  5796.  
  5797. this.vimeoPlayer = [];
  5798.  
  5799. if ($youtubeDiv.length) {
  5800. this.youtubeVideoId = $youtubeDiv.data('video-id');
  5801. this.initYoutubeVideo();
  5802. }
  5803.  
  5804. if ($vimeoDiv.length) {
  5805. this.vimeoVideoId = $vimeoDiv.data('video-id');
  5806. this.initVimeoVideo();
  5807. }
  5808.  
  5809. if ($mp4Div.length) {
  5810. startMp4Playback(mp4Video).then(function() {
  5811. // Video played as expected
  5812. setParentAsLoaded($mp4Div);
  5813. }).catch(function(error) {
  5814. // Video cannot be played with autoplay, so let
  5815. // user interact with video element itself
  5816. setParentAsLoaded($mp4Div);
  5817. setVideoToBeInteractedWith($mp4Div);
  5818. })
  5819. }
  5820. }
  5821.  
  5822. function startMp4Playback(mp4Video) {
  5823. return document.querySelector('#' + mp4Video).play();
  5824. }
  5825.  
  5826. function onVideoPlayerReady(evt) {
  5827. var $player = $(evt.target.a);
  5828. var playerId = $player.attr('id');
  5829. youtubePlayers[playerId] = evt.target; // update stored player
  5830. var player = youtubePlayers[playerId];
  5831.  
  5832. setParentAsLoading($player);
  5833.  
  5834. youtubePlayers[playerId].mute();
  5835.  
  5836. // Remove from tabindex because YouTube iframes are annoying and you can focus
  5837. // on the YouTube logo and it breaks
  5838. $player.attr('tabindex', '-1');
  5839.  
  5840. // Play video if in view
  5841. slate.utils.promiseStylesheet().then(function() {
  5842. videoVisibilityCheck(playerId);
  5843.  
  5844. // Add out of view pausing
  5845. $(window).on('scroll.' + playerId, {id: playerId}, $.throttle(150, videoVisibilityCheck));
  5846. });
  5847. }
  5848.  
  5849. function videoVisibilityCheck(id) {
  5850. var playerId;
  5851.  
  5852. if (!id) {
  5853. return;
  5854. }
  5855.  
  5856. if (typeof id === 'string') {
  5857. playerId = id;
  5858. } else {
  5859. // Data comes in as part of the scroll event
  5860. if (id && id.data) {
  5861. playerId = id.data.id;
  5862. } else {
  5863. return;
  5864. }
  5865. }
  5866.  
  5867. if (theme.isElementVisible($('#' + playerId))) {
  5868. playVisibleVideo(playerId);
  5869. } else {
  5870. pauseHiddenVideo(playerId);
  5871. }
  5872. }
  5873.  
  5874. function playVisibleVideo(id) {
  5875. if (youtubePlayers[id] && typeof youtubePlayers[id].playVideo === 'function') {
  5876. youtubePlayers[id].playVideo();
  5877. }
  5878. }
  5879.  
  5880. function pauseHiddenVideo(id) {
  5881. if (youtubePlayers[id] && typeof youtubePlayers[id].pauseVideo === 'function') {
  5882. youtubePlayers[id].pauseVideo();
  5883. }
  5884. }
  5885.  
  5886. function onVideoStateChange(evt) {
  5887. var $player = $(evt.target.a);
  5888. var playerId = $player.attr('id');
  5889. var player = youtubePlayers[playerId];
  5890.  
  5891. switch (evt.data) {
  5892. case -1: // unstarted
  5893. // Handle low power state on iOS by checking if
  5894. // video is reset to unplayed after attempting to buffer
  5895. if (videos[playerId].attemptedToPlay) {
  5896. setParentAsLoaded($player);
  5897. setVideoToBeInteractedWith($player);
  5898. }
  5899. break;
  5900. case 0: // ended
  5901. player.playVideo();
  5902. break;
  5903. case 1: // playing
  5904. setParentAsLoaded($player);
  5905. break;
  5906. case 3: // buffering
  5907. videos[playerId].attemptedToPlay = true;
  5908. break;
  5909. }
  5910. }
  5911.  
  5912. function setParentAsLoading($el) {
  5913. $el
  5914. .closest(selectors.videoParent)
  5915. .addClass(classes.loading);
  5916. }
  5917.  
  5918. function setParentAsLoaded($el) {
  5919. $el
  5920. .closest(selectors.videoParent)
  5921. .removeClass(classes.loading)
  5922. .addClass(classes.loaded);
  5923. }
  5924.  
  5925. function setVideoToBeInteractedWith($el) {
  5926. $el
  5927. .closest(selectors.videoParent)
  5928. .addClass(classes.interactable);
  5929. }
  5930.  
  5931. videoSection.prototype = $.extend({}, videoSection.prototype, {
  5932. initYoutubeVideo: function() {
  5933. videos[this.youtubePlayerId] = {
  5934. id: this.youtubePlayerId,
  5935. videoId: this.youtubeVideoId,
  5936. type: 'youtube',
  5937. attemptedToPlay: false
  5938. };
  5939.  
  5940. if (!youtubeReady) {
  5941. window.loadYouTube();
  5942. $('body').on('youTubeReady' + this.namespace, this.loadYoutubeVideo.bind(this));
  5943. } else {
  5944. this.loadYoutubeVideo();
  5945. }
  5946. },
  5947.  
  5948. loadYoutubeVideo: function() {
  5949. var args = $.extend({}, youtubeVideoOptions, videos[this.youtubePlayerId]);
  5950. args.playerVars.controls = 0;
  5951. youtubePlayers[this.youtubePlayerId] = new YT.Player(this.youtubePlayerId, args);
  5952.  
  5953. youtubeReady = true;
  5954. },
  5955.  
  5956. initVimeoVideo: function() {
  5957. videos[this.vimeoPlayerId] = {
  5958. divId: this.vimeoPlayerId,
  5959. id: this.vimeoVideoId,
  5960. type: 'vimeo'
  5961. };
  5962.  
  5963. var $player = $('#' + this.vimeoPlayerId);
  5964. setParentAsLoading($player);
  5965.  
  5966. // Button to play video on mobile
  5967. this.$vimeoTrigger.on('click', + this.namespace, function(evt) {
  5968. // $(evt.currentTarget).addClass('hide');
  5969. this.requestToPlayVimeoVideo(this.vimeoPlayerId);
  5970. }.bind(this));
  5971.  
  5972. if (!vimeoReady) {
  5973. window.loadVimeo();
  5974. $('body').on('vimeoReady' + this.namespace, this.loadVimeoVideo.bind(this));
  5975. } else {
  5976. this.loadVimeoVideo();
  5977. }
  5978. },
  5979.  
  5980. loadVimeoVideo: function() {
  5981. var args = $.extend({}, vimeoVideoOptions, videos[this.vimeoPlayerId]);
  5982. this.vimeoPlayer[this.vimeoPlayerId] = new Vimeo.Player(videos[this.vimeoPlayerId].divId, args);
  5983.  
  5984. vimeoReady = true;
  5985.  
  5986. // Only autoplay on larger screens
  5987. if (!theme.config.bpSmall) {
  5988. this.requestToPlayVimeoVideo(this.vimeoPlayerId);
  5989. } else {
  5990. var $player = $('#' + this.vimeoPlayerId);
  5991. setParentAsLoaded($player);
  5992. }
  5993. },
  5994.  
  5995. requestToPlayVimeoVideo: function(id) {
  5996. // The slider may initialize and attempt to play the video before
  5997. // the API is even ready, because it sucks.
  5998.  
  5999. if (!vimeoReady) {
  6000. // Wait for the trigger, then play it
  6001. $('body').on('vimeoReady' + this.namespace, function() {
  6002. this.playVimeoVideo(id);
  6003. }.bind(this))
  6004. return;
  6005. }
  6006.  
  6007. this.playVimeoVideo(id);
  6008. },
  6009.  
  6010. playVimeoVideo: function(id) {
  6011. this.vimeoPlayer[id].play();
  6012. this.vimeoPlayer[id].setVolume(0);
  6013.  
  6014. var $player = $('#' + id);
  6015. setParentAsLoaded($player);
  6016. },
  6017.  
  6018. onUnload: function(evt) {
  6019. var sectionId = evt.target.id.replace('shopify-section-', '');
  6020. var playerId = 'YouTubeVideo-' + sectionId;
  6021. if (youtubePlayers[playerId]) {
  6022. youtubePlayers[playerId].destroy();
  6023. }
  6024. $(window).off('scroll' + this.namespace);
  6025. $('body').off('vimeoReady' + this.namespace);
  6026. }
  6027. });
  6028.  
  6029. return videoSection;
  6030. })();
  6031.  
  6032. theme.Testimonials = (function() {
  6033. var slideCount = 0;
  6034. var defaults = {
  6035. accessibility: true,
  6036. arrows: false,
  6037. dots: true,
  6038. autoplay: false,
  6039. touchThreshold: 20,
  6040. slidesToShow: 3,
  6041. slidesToScroll: 3
  6042. };
  6043.  
  6044. function Testimonials(container) {
  6045. var $container = this.$container = $(container);
  6046. var sectionId = $container.attr('data-section-id');
  6047. var wrapper = this.wrapper = '.testimonials-wrapper';
  6048. var slider = this.slider = '#Testimonials-' + sectionId;
  6049. var $slider = $(slider);
  6050.  
  6051. this.sliderActive = false;
  6052. var mobileOptions = $.extend({}, defaults, {
  6053. slidesToShow: 1,
  6054. slidesToScroll: 1,
  6055. adaptiveHeight: true
  6056. });
  6057.  
  6058. slideCount = $slider.data('count');
  6059.  
  6060. // Override slidesToShow/Scroll if there are not enough blocks
  6061. if (slideCount < defaults.slidesToShow) {
  6062. defaults.slidesToShow = slideCount;
  6063. defaults.slidesToScroll = slideCount;
  6064. }
  6065.  
  6066. $slider.on('init', this.a11y.bind(this));
  6067.  
  6068. if (theme.config.bpSmall) {
  6069. this.init($slider, mobileOptions);
  6070. } else {
  6071. this.init($slider, defaults);
  6072. }
  6073.  
  6074. $('body').on('matchSmall', function() {
  6075. this.init($slider, mobileOptions);
  6076. }.bind(this));
  6077.  
  6078. $('body').on('matchLarge', function() {
  6079. this.init($slider, defaults);
  6080. }.bind(this));
  6081. }
  6082.  
  6083. Testimonials.prototype = $.extend({}, Testimonials.prototype, {
  6084. onUnload: function() {
  6085. $(this.slider, this.wrapper).slick('unslick');
  6086. },
  6087.  
  6088. onBlockSelect: function(evt) {
  6089. // Ignore the cloned version
  6090. var $slide = $('.testimonials-slide--' + evt.detail.blockId + ':not(.slick-cloned)');
  6091. var slideIndex = $slide.data('slick-index');
  6092.  
  6093. // Go to selected slide, pause autoplay
  6094. $(this.slider, this.wrapper).slick('slickGoTo', slideIndex);
  6095. },
  6096.  
  6097. init: function(obj, args) {
  6098. if (this.sliderActive) {
  6099. obj.slick('unslick');
  6100. this.sliderActive = false;
  6101. }
  6102.  
  6103. obj.slick(args);
  6104. this.sliderActive = true;
  6105.  
  6106. if (AOS) {
  6107. AOS.refresh();
  6108. }
  6109. },
  6110.  
  6111. a11y: function(event, obj) {
  6112. var $list = obj.$list;
  6113. var $wrapper = $(this.wrapper, this.$container);
  6114.  
  6115. // Remove default Slick aria-live attr until slider is focused
  6116. $list.removeAttr('aria-live');
  6117.  
  6118. // When an element in the slider is focused set aria-live
  6119. $wrapper.on('focusin', function(evt) {
  6120. if ($wrapper.has(evt.target).length) {
  6121. $list.attr('aria-live', 'polite');
  6122. }
  6123. });
  6124.  
  6125. // Remove aria-live
  6126. $wrapper.on('focusout', function(evt) {
  6127. if ($wrapper.has(evt.target).length) {
  6128. $list.removeAttr('aria-live');
  6129. }
  6130. });
  6131. }
  6132. });
  6133.  
  6134. return Testimonials;
  6135. })();
  6136.  
  6137. theme.NewsletterPopup = (function() {
  6138. function NewsletterPopup(container) {
  6139. var $container = this.$container = $(container);
  6140. var sectionId = $container.attr('data-section-id');
  6141. this.cookieName = 'newsletter-' + sectionId;
  6142.  
  6143. if (!$container.length) {
  6144. return;
  6145. }
  6146.  
  6147. // Prevent popup on Shopify robot challenge page
  6148. if (window.location.pathname === '/challenge') {
  6149. return;
  6150. }
  6151.  
  6152. this.data = {
  6153. secondsBeforeShow: $container.data('delay-seconds'),
  6154. daysBeforeReappear: $container.data('delay-days'),
  6155. cookie: Cookies.get(this.cookieName),
  6156. testMode: $container.data('test-mode')
  6157. };
  6158.  
  6159. this.modal = new theme.Modals('NewsletterPopup-' + sectionId, 'newsletter-popup-modal');
  6160.  
  6161. // Open modal if errors or success message exist
  6162. if ($container.find('.errors').length || $container.find('.note--success').length) {
  6163. this.modal.open();
  6164. }
  6165.  
  6166. // Set cookie as opened if success message
  6167. if ($container.find('.note--success').length) {
  6168. this.closePopup(true);
  6169. return;
  6170. }
  6171.  
  6172. $('body').on('modalClose.' + $container.attr('id'), this.closePopup.bind(this));
  6173.  
  6174. if (!this.data.cookie || this.data.testMode) {
  6175. this.initPopupDelay();
  6176. }
  6177. }
  6178.  
  6179. NewsletterPopup.prototype = $.extend({}, NewsletterPopup.prototype, {
  6180. initPopupDelay: function() {
  6181. setTimeout(function() {
  6182. this.modal.open();
  6183. }.bind(this), this.data.secondsBeforeShow * 1000);
  6184. },
  6185.  
  6186. closePopup: function(success) {
  6187. // Remove a cookie in case it was set in test mode
  6188. if (this.data.testMode) {
  6189. Cookies.remove(this.cookieName, { path: '/' });
  6190. return;
  6191. }
  6192.  
  6193. var expiry = success ? 200 : this.data.daysBeforeReappear;
  6194.  
  6195. Cookies.set(this.cookieName, 'opened', { path: '/', expires: expiry });
  6196. },
  6197.  
  6198. onLoad: function() {
  6199. this.modal.open();
  6200. },
  6201.  
  6202. onSelect: function() {
  6203. this.modal.open();
  6204. },
  6205.  
  6206. onDeselect: function() {
  6207. this.modal.close();
  6208. },
  6209.  
  6210. onUnload: function() {}
  6211. });
  6212.  
  6213. return NewsletterPopup;
  6214. })();
  6215.  
  6216. theme.Maps = (function() {
  6217. var config = {
  6218. zoom: 14
  6219. };
  6220. var apiStatus = null;
  6221. var mapsToLoad = [];
  6222.  
  6223. var errors = {
  6224. addressNoResults: theme.strings.addressNoResults,
  6225. addressQueryLimit: theme.strings.addressQueryLimit,
  6226. addressError: theme.strings.addressError,
  6227. authError: theme.strings.authError
  6228. };
  6229.  
  6230. var selectors = {
  6231. section: '[data-section-type="map"]',
  6232. map: '[data-map]',
  6233. mapOverlay: '[data-map-overlay]'
  6234. };
  6235.  
  6236. var classes = {
  6237. mapError: 'map-section--load-error',
  6238. errorMsg: 'map-section__error errors text-center'
  6239. };
  6240.  
  6241. // Global function called by Google on auth errors.
  6242. // Show an auto error message on all map instances.
  6243. window.gm_authFailure = function() {
  6244. if (!Shopify.designMode) {
  6245. return;
  6246. }
  6247.  
  6248. $(selectors.section).addClass(classes.mapError);
  6249. $(selectors.map).remove();
  6250. $(selectors.mapOverlay).after(
  6251. '<div class="' +
  6252. classes.errorMsg +
  6253. '">' +
  6254. theme.strings.authError +
  6255. '</div>'
  6256. );
  6257. };
  6258.  
  6259. function Map(container) {
  6260. this.$container = $(container);
  6261. this.sectionId = this.$container.attr('data-section-id');
  6262. this.namespace = '.map-' + this.sectionId;
  6263. this.$map = this.$container.find(selectors.map);
  6264. this.key = this.$map.data('api-key');
  6265.  
  6266. if (!this.key) {
  6267. return;
  6268. }
  6269.  
  6270. // Lazyload API
  6271. this.checkVisibility();
  6272. $(window).on('scroll' + this.namespace, $.throttle(50, this.checkVisibility.bind(this)));
  6273. }
  6274.  
  6275. function initAllMaps() {
  6276. // API has loaded, load all Map instances in queue
  6277. $.each(mapsToLoad, function(index, instance) {
  6278. instance.createMap();
  6279. });
  6280. }
  6281.  
  6282. function geolocate($map) {
  6283. var deferred = $.Deferred();
  6284. var geocoder = new google.maps.Geocoder();
  6285. var address = $map.data('address-setting');
  6286.  
  6287. geocoder.geocode({ address: address }, function(results, status) {
  6288. if (status !== google.maps.GeocoderStatus.OK) {
  6289. deferred.reject(status);
  6290. }
  6291.  
  6292. deferred.resolve(results);
  6293. });
  6294.  
  6295. return deferred;
  6296. }
  6297.  
  6298. Map.prototype = $.extend({}, Map.prototype, {
  6299. prepMapApi: function() {
  6300. if (apiStatus === 'loaded') {
  6301. this.createMap();
  6302. } else {
  6303. mapsToLoad.push(this);
  6304.  
  6305. if (apiStatus !== 'loading') {
  6306. apiStatus = 'loading';
  6307. if (typeof window.google === 'undefined' || typeof window.google.maps === 'undefined' ) {
  6308. $.getScript(
  6309. 'https://maps.googleapis.com/maps/api/js?key=' + this.key
  6310. ).then(function() {
  6311. apiStatus = 'loaded';
  6312. initAllMaps();
  6313. });
  6314. }
  6315. }
  6316. }
  6317. },
  6318.  
  6319. createMap: function() {
  6320. var $map = this.$map;
  6321.  
  6322. return geolocate($map)
  6323. .then(
  6324. function(results) {
  6325. var mapOptions = {
  6326. zoom: config.zoom,
  6327. backgroundColor: 'none',
  6328. center: results[0].geometry.location,
  6329. draggable: false,
  6330. clickableIcons: false,
  6331. scrollwheel: false,
  6332. disableDoubleClickZoom: true,
  6333. disableDefaultUI: true
  6334. };
  6335.  
  6336. var map = (this.map = new google.maps.Map($map[0], mapOptions));
  6337. var center = (this.center = map.getCenter());
  6338.  
  6339. var marker = new google.maps.Marker({
  6340. map: map,
  6341. position: map.getCenter()
  6342. });
  6343.  
  6344. google.maps.event.addDomListener(
  6345. window,
  6346. 'resize',
  6347. $.debounce(250, function() {
  6348. google.maps.event.trigger(map, 'resize');
  6349. map.setCenter(center);
  6350. $map.removeAttr('style');
  6351. })
  6352. );
  6353. }.bind(this)
  6354. )
  6355. .fail(function() {
  6356. var errorMessage;
  6357.  
  6358. switch (status) {
  6359. case 'ZERO_RESULTS':
  6360. errorMessage = errors.addressNoResults;
  6361. break;
  6362. case 'OVER_QUERY_LIMIT':
  6363. errorMessage = errors.addressQueryLimit;
  6364. break;
  6365. case 'REQUEST_DENIED':
  6366. errorMessage = errors.authError;
  6367. break;
  6368. default:
  6369. errorMessage = errors.addressError;
  6370. break;
  6371. }
  6372.  
  6373. // Show errors only to merchant in the editor.
  6374. if (Shopify.designMode) {
  6375. $map
  6376. .parent()
  6377. .addClass(classes.mapError)
  6378. .html(
  6379. '<div class="' +
  6380. classes.errorMsg +
  6381. '">' +
  6382. errorMessage +
  6383. '</div>'
  6384. );
  6385. }
  6386. });
  6387. },
  6388.  
  6389. checkVisibility: function() {
  6390. if (theme.isElementVisible(this.$container, 600)) {
  6391. this.prepMapApi();
  6392. $(window).off(this.namespace);
  6393. }
  6394. },
  6395.  
  6396. onUnload: function() {
  6397. if (this.$map.length === 0) {
  6398. return;
  6399. }
  6400. // Causes a harmless JS error when a section without an active map is reloaded
  6401. google.maps.event.clearListeners(this.map, 'resize');
  6402. }
  6403. });
  6404.  
  6405. return Map;
  6406. })();
  6407.  
  6408. theme.Blog = (function() {
  6409.  
  6410. function Blog(container) {
  6411. this.tagFilters();
  6412. }
  6413.  
  6414. Blog.prototype = $.extend({}, Blog.prototype, {
  6415. tagFilters: function() {
  6416. var $filterBy = $('#BlogTagFilter');
  6417.  
  6418. if (!$filterBy.length) {
  6419. return;
  6420. }
  6421.  
  6422. $filterBy.on('change', function() {
  6423. location.href = $(this).val();
  6424. });
  6425. },
  6426.  
  6427. onUnload: function() {
  6428.  
  6429. }
  6430. });
  6431.  
  6432. return Blog;
  6433. })();
  6434.  
  6435. theme.Photoswipe = (function() {
  6436. var selectors = {
  6437. trigger: '.product__photo-zoom',
  6438. images: '.photoswipe__image',
  6439. activeImage: '.slick-active .photoswipe__image'
  6440. };
  6441.  
  6442. function Photoswipe($container, sectionId) {
  6443. this.$container = $container;
  6444. this.sectionId = sectionId;
  6445. this.namespace = '.photoswipe-' + this.sectionId;
  6446. this.gallery;
  6447. this.$images;
  6448. this.inSlideshow = false;
  6449.  
  6450. if ($container.attr('data-zoom') === 'false') {
  6451. return;
  6452. }
  6453.  
  6454. if ($container.attr('data-has-slideshow') === 'true') {
  6455. this.inSlideshow = true;
  6456. }
  6457.  
  6458. this.init();
  6459. }
  6460.  
  6461. Photoswipe.prototype = $.extend({}, Photoswipe.prototype, {
  6462. init: function() {
  6463. var $trigger = this.$container.find(selectors.trigger);
  6464. this.$images = this.$container.find(selectors.images);
  6465. var items = [];
  6466.  
  6467. // Init gallery on active image
  6468. $trigger.on('click' + this.namespace, function(evt) {
  6469. items = this.getImageData();
  6470. if (this.inSlideshow || theme.config.bpSmall) {
  6471. var index = this.$container.find(selectors.activeImage).data('index');
  6472. } else {
  6473. var index = $(evt.currentTarget).data('index');
  6474. }
  6475. this.initGallery(items, index);
  6476. }.bind(this));
  6477. },
  6478.  
  6479. getImageData: function() {
  6480. var haveImages = false;
  6481. var items = [];
  6482. var options = {};
  6483.  
  6484. this.$images.each(function() {
  6485. var haveImages = true;
  6486. var smallSrc = $(this).prop('currentSrc') || $(this).prop('src');
  6487. var item = {
  6488. msrc: smallSrc,
  6489. src: $(this).data('photoswipe-src'),
  6490. w: $(this).data('photoswipe-width'),
  6491. h: $(this).data('photoswipe-height'),
  6492. el: $(this)[0],
  6493. initialZoomLevel: 0.5
  6494. };
  6495.  
  6496. items.push(item);
  6497. });
  6498.  
  6499. return items;
  6500. },
  6501.  
  6502. initGallery: function(items, index) {
  6503. var pswpElement = document.querySelectorAll('.pswp')[0];
  6504.  
  6505. var options = {
  6506. allowPanToNext: false,
  6507. captionEl: false,
  6508. closeOnScroll: false,
  6509. counterEl: false,
  6510. history: false,
  6511. index: index - 1,
  6512. pinchToClose: false,
  6513. preloaderEl: false,
  6514. scaleMode: 'zoom',
  6515. shareEl: false,
  6516. tapToToggleControls: false,
  6517. getThumbBoundsFn: function(index) {
  6518. var pageYScroll = window.pageYOffset || document.documentElement.scrollTop;
  6519. var thumbnail = items[index].el;
  6520. var rect = thumbnail.getBoundingClientRect();
  6521. return {x:rect.left, y:rect.top + pageYScroll, w:rect.width};
  6522. }
  6523. }
  6524.  
  6525. this.gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options);
  6526.  
  6527. this.gallery.init();
  6528. this.gallery.listen('afterChange', this.afterChange.bind(this));
  6529.  
  6530. // If need to destroy it
  6531. // this.gallery.destroy();
  6532. },
  6533.  
  6534. afterChange: function() {
  6535. if (this.inSlideshow) {
  6536. var $slideshow = $('#ProductPhotos-' + this.sectionId);
  6537. if ($slideshow.hasClass('slick-initialized')) {
  6538. var newIndex = this.gallery.getCurrentIndex();
  6539. $slideshow.slick('slickGoTo', newIndex);
  6540. }
  6541. }
  6542. }
  6543. });
  6544.  
  6545. return Photoswipe;
  6546. })();
  6547.  
  6548.  
  6549.  
  6550. // Breakpoint values are used throughout many templates.
  6551. // We strongly suggest not changing them globally.
  6552. theme.bp = {};
  6553. theme.bp.smallUp = 769;
  6554. theme.bp.small = theme.bp.smallUp - 1;
  6555.  
  6556. theme.config = {
  6557. cssLoaded: false,
  6558. bpSmall: false,
  6559. hasSessionStorage: true,
  6560. mediaQuerySmall: 'screen and (max-width: '+ theme.bp.small +'px)',
  6561. mediaQuerySmallUp: 'screen and (min-width: '+ theme.bp.smallUp +'px)',
  6562. youTubeReady: false,
  6563. vimeoReady: false,
  6564. vimeoLoading: false,
  6565. isSafari: !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/),
  6566. isTouch: ('ontouchstart' in window) || window.DocumentTouch && window.document instanceof DocumentTouch || window.navigator.maxTouchPoints || window.navigator.msMaxTouchPoints ? true : false
  6567. };
  6568.  
  6569. window.onYouTubeIframeAPIReady = function() {
  6570. theme.config.youTubeReady = true;
  6571. $('body').trigger('youTubeReady');
  6572. };
  6573.  
  6574. window.loadYouTube = function() {
  6575. if (theme.config.youtubeReady) {
  6576. return;
  6577. }
  6578.  
  6579. var tag = document.createElement('script');
  6580. tag.src = "https://www.youtube.com/iframe_api";
  6581. var firstScriptTag = document.getElementsByTagName('script')[0];
  6582. firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
  6583. };
  6584.  
  6585. window.loadVimeo = function() {
  6586. if (theme.config.vimeoLoading) {
  6587. return;
  6588. }
  6589.  
  6590. if (!theme.config.vimeoReady) {
  6591. theme.config.vimeoLoading = true;
  6592. var tag = document.createElement('script');
  6593. tag.src = "https://player.vimeo.com/api/player.js";
  6594. var firstScriptTag = document.getElementsByTagName('script')[0];
  6595. firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
  6596.  
  6597. // Because there's no way to check for the Vimeo API being loaded
  6598. // asynchronously, we use this terrible timeout to wait for it being ready
  6599. checkIfVimeoIsReady()
  6600. .then(function() {
  6601. theme.config.vimeoReady = true;
  6602. theme.config.vimeoLoading = false;
  6603. $('body').trigger('vimeoReady');
  6604. })
  6605. .fail(function() {
  6606. // No vimeo API to talk to
  6607. });
  6608. }
  6609. };
  6610.  
  6611. function checkIfVimeoIsReady() {
  6612. var deferred = $.Deferred();
  6613. var wait;
  6614. var timeout;
  6615.  
  6616. wait = setInterval(function() {
  6617. if (!Vimeo) {
  6618. return;
  6619. }
  6620.  
  6621. clearInterval(wait);
  6622. clearTimeout(timeout);
  6623. deferred.resolve();
  6624. }, 500);
  6625.  
  6626. timeout = setTimeout(function() {
  6627. clearInterval(wait);
  6628. deferred.reject();
  6629. }, 4000); // subjective. test up to 8 times over 4 seconds
  6630.  
  6631. return deferred;
  6632. };
  6633.  
  6634. theme.init = function() {
  6635. theme.setGlobals();
  6636. theme.pageTransitions();
  6637. theme.QuickShopScreens.init();
  6638. theme.articleImages.init();
  6639. theme.collapsibles.init();
  6640. if (theme.settings.cartType === 'sticky') {
  6641. new theme.StickyCart.init();
  6642. }
  6643. theme.customerTemplates.init();
  6644. if (theme.settings.currenciesEnabled) {
  6645. theme.currencySwitcher.init();
  6646. }
  6647. theme.videoModal();
  6648. slate.rte.init();
  6649.  
  6650. $(document.documentElement).on('keyup.tab', function(evt) {
  6651. if (evt.keyCode === 9) {
  6652. $(document.documentElement).addClass('tab-outline');
  6653. $(document.documentElement).off('keyup.tab');
  6654. }
  6655. });
  6656.  
  6657. // Two ways to determine if page was loaded from cache from back button
  6658. // Most use `pageshow` + evt.persisted, Chrome uses `performance.navigation.type`
  6659. window.addEventListener('pageshow', function(evt) {
  6660. if (evt.persisted) {
  6661. theme.refreshCart();
  6662. }
  6663. });
  6664.  
  6665. if (performance && performance.navigation.type === 2) {
  6666. theme.refreshCart();
  6667. }
  6668. };
  6669.  
  6670. theme.refreshCart = function() {
  6671. if (theme.settings.cartType === 'sticky' && theme.StickyCart) {
  6672. $.getJSON('/cart.js').then(function(cart) {
  6673. theme.StickyCart.refresh(cart);
  6674. })
  6675. }
  6676. };
  6677.  
  6678. theme.setGlobals = function() {
  6679. theme.config.hasSessionStorage = theme.isSessionStorageSupported();
  6680.  
  6681. if (theme.config.isTouch) {
  6682. $('body').addClass('supports-touch');
  6683. }
  6684.  
  6685. enquire.register(theme.config.mediaQuerySmall, {
  6686. match: function() {
  6687. theme.config.bpSmall = true;
  6688. $('body').trigger('matchSmall');
  6689. },
  6690. unmatch: function() {
  6691. theme.config.bpSmall = false;
  6692. $('body').trigger('unmatchSmall');
  6693. }
  6694. });
  6695.  
  6696. enquire.register(theme.config.mediaQuerySmallUp, {
  6697. match: function() {
  6698. $('body').trigger('matchLarge');
  6699. },
  6700. unmatch: function() {
  6701. $('body').trigger('unmatchLarge');
  6702. }
  6703. });
  6704. };
  6705.  
  6706. theme.loadImageSection = function($container) {
  6707. // Wait until images inside container have lazyloaded class
  6708. function setAsLoaded() {
  6709. $container.removeClass('loading').addClass('loaded');
  6710. }
  6711.  
  6712. function checkForLazyloadedImage() {
  6713. return $container.find('.lazyloaded').length;
  6714. }
  6715.  
  6716. // If it has SVGs it's in the onboarding state so set as loaded
  6717. if ($container.find('svg').length) {
  6718. setAsLoaded();
  6719. return;
  6720. };
  6721.  
  6722. if (checkForLazyloadedImage() > 0) {
  6723. setAsLoaded();
  6724. return;
  6725. }
  6726.  
  6727. var interval = setInterval(function() {
  6728. if (checkForLazyloadedImage() > 0) {
  6729. clearInterval(interval);
  6730. setAsLoaded();
  6731. }
  6732. }, 80);
  6733. }
  6734.  
  6735. theme.isSessionStorageSupported = function() {
  6736. // Return false if we are in an iframe without access to sessionStorage
  6737. if (window.self !== window.top) {
  6738. return false;
  6739. }
  6740.  
  6741. var testKey = 'test';
  6742. var storage = window.sessionStorage;
  6743. try {
  6744. storage.setItem(testKey, '1');
  6745. storage.removeItem(testKey);
  6746. return true;
  6747. } catch (error) {
  6748. return false;
  6749. }
  6750. };
  6751.  
  6752. theme.isElementVisible = function($el, threshold) {
  6753. var rect = $el[0].getBoundingClientRect();
  6754. var windowHeight = window.innerHeight || document.documentElement.clientHeight;
  6755. threshold = threshold ? threshold : 0;
  6756.  
  6757. return (
  6758. rect.bottom >= (0 - (threshold / 1.5)) &&
  6759. rect.right >= 0 &&
  6760. rect.top <= (windowHeight + threshold) &&
  6761. rect.left <= (window.innerWidth || document.documentElement.clientWidth)
  6762. );
  6763. };
  6764.  
  6765. theme.pageTransitions = function() {
  6766. if ($('body').data('transitions') == true) {
  6767.  
  6768. // Hack test to fix Safari page cache issue.
  6769. // window.onpageshow doesn't always run when navigating
  6770. // back to the page, so the unloading class remains, leaving
  6771. // a white page. Setting a timeout to remove that class when leaving
  6772. // the page actually finishes running when they come back.
  6773. if (theme.config.isSafari) {
  6774. $('a').on('click', function() {
  6775. window.setTimeout(function() {
  6776. $('body').removeClass('unloading');
  6777. }, 1200);
  6778. });
  6779. }
  6780.  
  6781. // Add disable transition class to malito, anchor, and YouTube links
  6782. $('a[href^="mailto:"], a[href^="#"], a[target="_blank"], a[href*="youtube.com/watch"], a[href*="youtu.be/"]').each(function() {
  6783. $(this).addClass('js-no-transition');
  6784. });
  6785.  
  6786. $('a:not(.js-no-transition)').on('click', function(evt) {
  6787. if (evt.metaKey) return true;
  6788.  
  6789. var src = $(this).attr('href');
  6790.  
  6791. // Bail if it's a hash link
  6792. if(src.indexOf(location.pathname) >= 0 && src.indexOf('#') >= 0) {
  6793. return true;
  6794. }
  6795.  
  6796. evt.preventDefault();
  6797. $('body').addClass('unloading');
  6798. window.setTimeout(function() {
  6799. location.href = src;
  6800. }, 50);
  6801. });
  6802. }
  6803. };
  6804.  
  6805. window.onpageshow = function(evt) {
  6806. // Removes unload class when returning to page via history
  6807. if (evt.persisted) {
  6808. $('body').removeClass('unloading');
  6809. }
  6810. };
  6811.  
  6812. theme.initSecondary = function() {
  6813. document.body.classList.add('js-animate');
  6814. AOS.init({
  6815. easing: 'ease-out-quad',
  6816. once: false,
  6817. mirror: true,
  6818. offset: 100,
  6819. disableMutationObserver: true
  6820. });
  6821.  
  6822. document.addEventListener('lazyloaded', function(evt) {
  6823. var $img = $(evt.target);
  6824. if ($img.length) {
  6825. $img.parent().addClass('loaded');
  6826. }
  6827. });
  6828.  
  6829. document.dispatchEvent(new CustomEvent('page:loaded'));
  6830.  
  6831. theme.reviewAppLinkListener();
  6832. theme.checkForAnchorLink();
  6833. };
  6834.  
  6835. theme.reviewAppLinkListener = function() {
  6836. $('body').on('click', '.spr-pagination', function() {
  6837. var $scroller = $(this).closest('.spr-reviews').scrollLeft(0);
  6838. });
  6839. };
  6840.  
  6841. theme.checkForAnchorLink = function() {
  6842. if(window.location.hash) {
  6843. var $el = $(window.location.hash);
  6844. if ($el.length) {
  6845. var top = $el.offset().top - 100;
  6846. if (top > 0) {
  6847. window.scroll(0, top);
  6848. }
  6849. }
  6850. }
  6851. };
  6852.  
  6853. theme.reinitProductGridItem = function($scope) {
  6854. if (AOS) {
  6855. AOS.refreshHard();
  6856. }
  6857.  
  6858. // Refresh currency
  6859. if (theme.settings.currenciesEnabled) {
  6860. theme.currencySwitcher.ajaxrefresh();
  6861. }
  6862.  
  6863. // Refresh reviews app
  6864. if (window.SPR) {
  6865. SPR.initDomEls();SPR.loadBadges();
  6866. }
  6867.  
  6868. // Re-register product templates in quick view modals.
  6869. // Will not double-register.
  6870. // theme.initQuickShop(true);
  6871. // sections.register('product-template', theme.Product, $scope);
  6872.  
  6873. // Re-hook up collapsible box triggers
  6874. theme.collapsibles.init();
  6875. };
  6876.  
  6877. $(document).ready(function() {
  6878. theme.init();
  6879.  
  6880. // Init CSS-dependent scripts
  6881. slate.utils.promiseStylesheet().then(function() {
  6882. theme.initSecondary();
  6883. });
  6884.  
  6885. window.sections = new theme.Sections();
  6886. sections.register('header-section', theme.HeaderSection);
  6887. sections.register('slideshow-section', theme.SlideshowSection);
  6888. sections.register('hero-animated', theme.HeroAnimated);
  6889. sections.register('video-section', theme.VideoSection);
  6890. sections.register('product', theme.Product);
  6891. sections.register('product-recommendations', theme.Recommendations);
  6892. sections.register('product-template', theme.Product);
  6893. sections.register('featured-collection', theme.FeaturedCollection);
  6894. sections.register('collection-template', theme.Collection);
  6895. sections.register('featured-content-section', theme.FeaturedContentSection);
  6896. sections.register('testimonials', theme.Testimonials);
  6897. sections.register('newsletter-popup', theme.NewsletterPopup);
  6898. sections.register('map', theme.Maps);
  6899. sections.register('blog', theme.Blog);
  6900. });
  6901.  
  6902. })(theme.jQuery);
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement