Advertisement
Guest User

Untitled

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