{"version":3,"file":"default/js/productDetail.js","sources":["webpack://rws/./dependencies/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/components/clientSideValidation.js","webpack://rws/./dependencies/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/components/focus.js","webpack://rws/./dependencies/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/components/keyboardAccessibility.js","webpack://rws/./dependencies/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/product/base.js","webpack://rws/./dependencies/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/product/detail.js","webpack://rws/./dependencies/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/util.js","webpack://rws/./cartridges/app_rws/cartridge/client/default/js/cart/cart.js","webpack://rws/./cartridges/app_rws/cartridge/client/default/js/components/clientSideValidation.js","webpack://rws/./cartridges/app_rws/cartridge/client/default/js/components/zoom-carousel.js","webpack://rws/./cartridges/app_rws/cartridge/client/default/js/product/base.js","webpack://rws/./cartridges/app_rws/cartridge/client/default/js/product/detail.js","webpack://rws/./cartridges/app_rws/cartridge/client/default/js/product/productDetailsSection.js","webpack://rws/./cartridges/app_rws/cartridge/client/default/js/product/swatchableAttribute.js","webpack://rws/./cartridges/app_rws/cartridge/client/default/js/storeDrawer/storeDrawerPdp.js","webpack://rws/./cartridges/app_rws/cartridge/client/default/js/util/matchBreakpoints.js","webpack://rws/./cartridges/app_rws/cartridge/client/default/js/util/urlUtils.js","webpack://rws/./cartridges/app_rws/cartridge/client/default/js/productDetail.js"],"sourcesContent":["'use strict';\n\n/**\n * Validate whole form. Requires `this` to be set to form object\n * @param {jQuery.event} event - Event to be canceled if form is invalid.\n * @returns {boolean} - Flag to indicate if form is valid\n */\nfunction validateForm(event) {\n var valid = true;\n if (this.checkValidity && !this.checkValidity()) {\n // safari\n valid = false;\n if (event) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n }\n $(this).find('input, select').each(function () {\n if (!this.validity.valid) {\n $(this).trigger('invalid', this.validity);\n }\n });\n }\n return valid;\n}\n\n/**\n * Remove all validation. Should be called every time before revalidating form\n * @param {element} form - Form to be cleared\n * @returns {void}\n */\nfunction clearForm(form) {\n $(form).find('.form-control.is-invalid').removeClass('is-invalid');\n}\n\nmodule.exports = {\n invalid: function () {\n $('form input, form select').on('invalid', function (e) {\n e.preventDefault();\n this.setCustomValidity('');\n if (!this.validity.valid) {\n var validationMessage = this.validationMessage;\n $(this).addClass('is-invalid');\n if (this.validity.patternMismatch && $(this).data('pattern-mismatch')) {\n validationMessage = $(this).data('pattern-mismatch');\n }\n if ((this.validity.rangeOverflow || this.validity.rangeUnderflow)\n && $(this).data('range-error')) {\n validationMessage = $(this).data('range-error');\n }\n if ((this.validity.tooLong || this.validity.tooShort)\n && $(this).data('range-error')) {\n validationMessage = $(this).data('range-error');\n }\n if (this.validity.valueMissing && $(this).data('missing-error')) {\n validationMessage = $(this).data('missing-error');\n }\n $(this).parents('.form-group').find('.invalid-feedback')\n .text(validationMessage);\n }\n });\n },\n\n submit: function () {\n $('form').on('submit', function (e) {\n return validateForm.call(this, e);\n });\n },\n\n buttonClick: function () {\n $('form button[type=\"submit\"], form input[type=\"submit\"]').on('click', function () {\n // clear all errors when trying to submit the form\n clearForm($(this).parents('form'));\n });\n },\n\n functions: {\n validateForm: function (form, event) {\n validateForm.call($(form), event || null);\n },\n clearForm: clearForm\n }\n};\n","'use strict';\n\nmodule.exports = {\n setTabNextFocus: function (focusParams) {\n var KEYCODE_TAB = 9;\n var isTabPressed = (focusParams.event.key === 'Tab' || focusParams.event.keyCode === KEYCODE_TAB);\n\n if (!isTabPressed) {\n return;\n }\n\n var firstFocusableEl = $(focusParams.containerSelector + ' ' + focusParams.firstElementSelector);\n var lastFocusableEl = $(focusParams.containerSelector + ' ' + focusParams.lastElementSelector);\n\n if ($(focusParams.containerSelector + ' ' + focusParams.lastElementSelector).is(':disabled')) {\n lastFocusableEl = $(focusParams.containerSelector + ' ' + focusParams.nextToLastElementSelector);\n if ($('.product-quickview.product-set').length > 0) {\n var linkElements = $(focusParams.containerSelector + ' a#fa-link.share-icons');\n lastFocusableEl = linkElements[linkElements.length - 1];\n }\n }\n\n if (focusParams.event.shiftKey) /* shift + tab */ {\n if ($(':focus').is(firstFocusableEl)) {\n lastFocusableEl.focus();\n focusParams.event.preventDefault();\n }\n } else /* tab */ {\n if ($(':focus').is(lastFocusableEl)) { // eslint-disable-line\n firstFocusableEl.focus();\n focusParams.event.preventDefault();\n }\n }\n }\n};\n","'use strict';\n\n// Customization: Added Enter key (13)\n\nmodule.exports = function (selector, keyFunctions, preFunction) {\n $(selector).on('keydown', function (e) {\n var key = e.which;\n var supportedKeyCodes = [37, 38, 39, 40, 27, 13];\n if (supportedKeyCodes.indexOf(key) >= 0) {\n e.preventDefault();\n }\n var returnedScope = preFunction.call(this);\n if (keyFunctions[key]) {\n keyFunctions[key].call(this, returnedScope);\n }\n });\n};\n","'use strict';\nvar focusHelper = require('../components/focus');\n\n/**\n * Retrieves the relevant pid value\n * @param {jquery} $el - DOM container for a given add to cart button\n * @return {string} - value to be used when adding product to cart\n */\nfunction getPidValue($el) {\n var pid;\n\n if ($('#quickViewModal').hasClass('show') && !$('.product-set').length) {\n pid = $($el).closest('.modal-content').find('.product-quickview').data('pid');\n } else if ($('.product-set-detail').length || $('.product-set').length) {\n pid = $($el).closest('.product-detail').find('.product-id').text();\n } else {\n pid = $('.product-detail:not(\".bundle-item\")').data('pid');\n }\n\n return pid;\n}\n\n/**\n * Retrieve contextual quantity selector\n * @param {jquery} $el - DOM container for the relevant quantity\n * @return {jquery} - quantity selector DOM container\n */\nfunction getQuantitySelector($el) {\n var quantitySelected;\n if ($el && $('.set-items').length) {\n quantitySelected = $($el).closest('.product-detail').find('.quantity-select');\n } else if ($el && $('.product-bundle').length) {\n var quantitySelectedModal = $($el).closest('.modal-footer').find('.quantity-select');\n var quantitySelectedPDP = $($el).closest('.bundle-footer').find('.quantity-select');\n if (quantitySelectedModal.val() === undefined) {\n quantitySelected = quantitySelectedPDP;\n } else {\n quantitySelected = quantitySelectedModal;\n }\n } else {\n quantitySelected = $('.quantity-select');\n }\n return quantitySelected;\n}\n\n/**\n * Retrieves the value associated with the Quantity pull-down menu\n * @param {jquery} $el - DOM container for the relevant quantity\n * @return {string} - value found in the quantity input\n */\nfunction getQuantitySelected($el) {\n return getQuantitySelector($el).val();\n}\n\n/**\n * Process the attribute values for an attribute that has image swatches\n *\n * @param {Object} attr - Attribute\n * @param {string} attr.id - Attribute ID\n * @param {Object[]} attr.values - Array of attribute value objects\n * @param {string} attr.values.value - Attribute coded value\n * @param {string} attr.values.url - URL to de/select an attribute value of the product\n * @param {boolean} attr.values.isSelectable - Flag as to whether an attribute value can be\n * selected. If there is no variant that corresponds to a specific combination of attribute\n * values, an attribute may be disabled in the Product Detail Page\n * @param {jQuery} $productContainer - DOM container for a given product\n * @param {Object} msgs - object containing resource messages\n */\nfunction processSwatchValues(attr, $productContainer, msgs) {\n attr.values.forEach(function (attrValue) {\n var $attrValue = $productContainer.find('[data-attr=\"' + attr.id + '\"] [data-attr-value=\"' +\n attrValue.value + '\"]');\n var $swatchButton = $attrValue.parent();\n\n if (attrValue.selected) {\n $attrValue.addClass('selected');\n $attrValue.siblings('.selected-assistive-text').text(msgs.assistiveSelectedText);\n } else {\n $attrValue.removeClass('selected');\n $attrValue.siblings('.selected-assistive-text').empty();\n }\n\n if (attrValue.url) {\n $swatchButton.attr('data-url', attrValue.url);\n } else {\n $swatchButton.removeAttr('data-url');\n }\n\n // Disable if not selectable\n $attrValue.removeClass('selectable unselectable');\n\n $attrValue.addClass(attrValue.selectable ? 'selectable' : 'unselectable');\n });\n}\n\n/**\n * Process attribute values associated with an attribute that does not have image swatches\n *\n * @param {Object} attr - Attribute\n * @param {string} attr.id - Attribute ID\n * @param {Object[]} attr.values - Array of attribute value objects\n * @param {string} attr.values.value - Attribute coded value\n * @param {string} attr.values.url - URL to de/select an attribute value of the product\n * @param {boolean} attr.values.isSelectable - Flag as to whether an attribute value can be\n * selected. If there is no variant that corresponds to a specific combination of attribute\n * values, an attribute may be disabled in the Product Detail Page\n * @param {jQuery} $productContainer - DOM container for a given product\n */\nfunction processNonSwatchValues(attr, $productContainer) {\n var $attr = '[data-attr=\"' + attr.id + '\"]';\n var $defaultOption = $productContainer.find($attr + ' .select-' + attr.id + ' option:first');\n $defaultOption.attr('value', attr.resetUrl);\n\n attr.values.forEach(function (attrValue) {\n var $attrValue = $productContainer\n .find($attr + ' [data-attr-value=\"' + attrValue.value + '\"]');\n $attrValue.attr('value', attrValue.url)\n .removeAttr('disabled');\n\n if (!attrValue.selectable) {\n $attrValue.attr('disabled', true);\n }\n });\n}\n\n/**\n * Routes the handling of attribute processing depending on whether the attribute has image\n * swatches or not\n *\n * @param {Object} attrs - Attribute\n * @param {string} attr.id - Attribute ID\n * @param {jQuery} $productContainer - DOM element for a given product\n * @param {Object} msgs - object containing resource messages\n */\nfunction updateAttrs(attrs, $productContainer, msgs) {\n // Currently, the only attribute type that has image swatches is Color.\n var attrsWithSwatches = ['color'];\n\n attrs.forEach(function (attr) {\n if (attrsWithSwatches.indexOf(attr.id) > -1) {\n processSwatchValues(attr, $productContainer, msgs);\n } else {\n processNonSwatchValues(attr, $productContainer);\n }\n });\n}\n\n/**\n * Updates the availability status in the Product Detail Page\n *\n * @param {Object} response - Ajax response object after an\n * attribute value has been [de]selected\n * @param {jQuery} $productContainer - DOM element for a given product\n */\nfunction updateAvailability(response, $productContainer) {\n var availabilityValue = '';\n var availabilityMessages = response.product.availability.messages;\n if (!response.product.readyToOrder) {\n availabilityValue = '
';\n });\n }\n });\n\n return html;\n}\n\n/**\n * @typedef UpdatedOptionValue\n * @type Object\n * @property {string} id - Option value ID for look up\n * @property {string} url - Updated option value selection URL\n */\n\n/**\n * @typedef OptionSelectionResponse\n * @type Object\n * @property {string} priceHtml - Updated price HTML code\n * @property {Object} options - Updated Options\n * @property {string} options.id - Option ID\n * @property {UpdatedOptionValue[]} options.values - Option values\n */\n\n/**\n * Updates DOM using post-option selection Ajax response\n *\n * @param {OptionSelectionResponse} optionsHtml - Ajax response optionsHtml from selecting a product option\n * @param {jQuery} $productContainer - DOM element for current product\n */\nfunction updateOptions(optionsHtml, $productContainer) {\n\t// Update options\n $productContainer.find('.product-options').empty().html(optionsHtml);\n}\n\n/**\n * Dynamically creates Bootstrap carousel from response containing images\n * @param {Object[]} imgs - Array of large product images,along with related information\n * @param {jQuery} $productContainer - DOM element for a given product\n */\nfunction createCarousel(imgs, $productContainer) {\n var carousel = $productContainer.find('.carousel');\n $(carousel).carousel('dispose');\n var carouselId = $(carousel).attr('id');\n $(carousel).empty().append('' + $(carousel).data('prev') + '' + $(carousel).data('next') + '');\n for (var i = 0; i < imgs.length; i++) {\n $('
').appendTo($(carousel).find('.carousel-inner'));\n $('').appendTo($(carousel).find('.carousel-indicators'));\n }\n $($(carousel).find('.carousel-item')).first().addClass('active');\n $($(carousel).find('.carousel-indicators > li')).first().addClass('active');\n if (imgs.length === 1) {\n $($(carousel).find('.carousel-indicators, a[class^=\"carousel-control-\"]')).detach();\n }\n $(carousel).carousel();\n $($(carousel).find('.carousel-indicators')).attr('aria-hidden', true);\n}\n\n/**\n * Parses JSON from Ajax call made whenever an attribute value is [de]selected\n * @param {Object} response - response from Ajax call\n * @param {Object} response.product - Product object\n * @param {string} response.product.id - Product ID\n * @param {Object[]} response.product.variationAttributes - Product attributes\n * @param {Object[]} response.product.images - Product images\n * @param {boolean} response.product.hasRequiredAttrsSelected - Flag as to whether all required\n * attributes have been selected. Used partially to\n * determine whether the Add to Cart button can be enabled\n * @param {jQuery} $productContainer - DOM element for a given product.\n */\nfunction handleVariantResponse(response, $productContainer) {\n var isChoiceOfBonusProducts =\n $productContainer.parents('.choose-bonus-product-dialog').length > 0;\n var isVaraint;\n if (response.product.variationAttributes) {\n updateAttrs(response.product.variationAttributes, $productContainer, response.resources);\n isVaraint = response.product.productType === 'variant';\n if (isChoiceOfBonusProducts && isVaraint) {\n $productContainer.parent('.bonus-product-item')\n .data('pid', response.product.id);\n\n $productContainer.parent('.bonus-product-item')\n .data('ready-to-order', response.product.readyToOrder);\n }\n }\n\n // Update primary images\n var primaryImageUrls = response.product.images.large;\n createCarousel(primaryImageUrls, $productContainer);\n\n // Update pricing\n if (!isChoiceOfBonusProducts) {\n var $priceSelector = $('.prices .price', $productContainer).length\n ? $('.prices .price', $productContainer)\n : $('.prices .price');\n $priceSelector.replaceWith(response.product.price.html);\n }\n\n // Update promotions\n $productContainer.find('.promotions').empty().html(response.product.promotionsHtml);\n\n updateAvailability(response, $productContainer);\n\n if (isChoiceOfBonusProducts) {\n var $selectButton = $productContainer.find('.select-bonus-product');\n $selectButton.trigger('bonusproduct:updateSelectButton', {\n product: response.product, $productContainer: $productContainer\n });\n } else {\n // Enable \"Add to Cart\" button if all required attributes have been selected\n $('button.add-to-cart, button.add-to-cart-global, button.update-cart-product-global').trigger('product:updateAddToCart', {\n product: response.product, $productContainer: $productContainer\n }).trigger('product:statusUpdate', response.product);\n }\n\n // Update attributes\n $productContainer.find('.main-attributes').empty()\n .html(getAttributesHtml(response.product.attributes));\n}\n\n/**\n * @typespec UpdatedQuantity\n * @type Object\n * @property {boolean} selected - Whether the quantity has been selected\n * @property {string} value - The number of products to purchase\n * @property {string} url - Compiled URL that specifies variation attributes, product ID, options,\n * etc.\n */\n\n/**\n * Updates the quantity DOM elements post Ajax call\n * @param {UpdatedQuantity[]} quantities -\n * @param {jQuery} $productContainer - DOM container for a given product\n */\nfunction updateQuantities(quantities, $productContainer) {\n if ($productContainer.parent('.bonus-product-item').length <= 0) {\n var optionsHtml = quantities.map(function (quantity) {\n var selected = quantity.selected ? ' selected ' : '';\n return '';\n }).join('');\n getQuantitySelector($productContainer).empty().html(optionsHtml);\n }\n}\n\n/**\n * updates the product view when a product attribute is selected or deselected or when\n * changing quantity\n * @param {string} selectedValueUrl - the Url for the selected variation value\n * @param {jQuery} $productContainer - DOM element for current product\n */\nfunction attributeSelect(selectedValueUrl, $productContainer) {\n if (selectedValueUrl) {\n $('body').trigger('product:beforeAttributeSelect',\n { url: selectedValueUrl, container: $productContainer });\n\n $.ajax({\n url: selectedValueUrl,\n method: 'GET',\n success: function (data) {\n handleVariantResponse(data, $productContainer);\n updateOptions(data.product.optionsHtml, $productContainer);\n updateQuantities(data.product.quantities, $productContainer);\n $('body').trigger('product:afterAttributeSelect',\n { data: data, container: $productContainer });\n $.spinner().stop();\n },\n error: function () {\n $.spinner().stop();\n }\n });\n }\n}\n\n/**\n * Retrieves url to use when adding a product to the cart\n *\n * @return {string} - The provided URL to use when adding a product to the cart\n */\nfunction getAddToCartUrl() {\n return $('.add-to-cart-url').val();\n}\n\n/**\n * Parses the html for a modal window\n * @param {string} html - representing the body and footer of the modal window\n *\n * @return {Object} - Object with properties body and footer.\n */\nfunction parseHtml(html) {\n var $html = $('
').append($.parseHTML(html));\n\n var body = $html.find('.choice-of-bonus-product');\n var footer = $html.find('.modal-footer').children();\n\n return { body: body, footer: footer };\n}\n\n/**\n * Retrieves url to use when adding a product to the cart\n *\n * @param {Object} data - data object used to fill in dynamic portions of the html\n */\nfunction chooseBonusProducts(data) {\n $('.modal-body').spinner().start();\n\n if ($('#chooseBonusProductModal').length !== 0) {\n $('#chooseBonusProductModal').remove();\n }\n var bonusUrl;\n if (data.bonusChoiceRuleBased) {\n bonusUrl = data.showProductsUrlRuleBased;\n } else {\n bonusUrl = data.showProductsUrlListBased;\n }\n\n var htmlString = ''\n + '
'\n );\n setTimeout(function () {\n $('.add-to-basket-alert').remove();\n if ($('.cart-page').length) {\n location.reload();\n }\n }, 1500);\n }\n },\n error: function () {\n $.spinner().stop();\n }\n });\n });\n },\n\n getPidValue: getPidValue,\n getQuantitySelected: getQuantitySelected,\n miniCartReportingUrl: miniCartReportingUrl\n};\n","'use strict';\nvar base = require('./base');\n\n/**\n * Enable/disable UI elements\n * @param {boolean} enableOrDisable - true or false\n */\nfunction updateAddToCartEnableDisableOtherElements(enableOrDisable) {\n $('button.add-to-cart-global').attr('disabled', enableOrDisable);\n}\n\nmodule.exports = {\n methods: {\n updateAddToCartEnableDisableOtherElements: updateAddToCartEnableDisableOtherElements\n },\n\n availability: base.availability,\n\n addToCart: base.addToCart,\n\n updateAttributesAndDetails: function () {\n $('body').on('product:statusUpdate', function (e, data) {\n var $productContainer = $('.product-detail[data-pid=\"' + data.id + '\"]');\n\n $productContainer.find('.description-and-detail .product-attributes')\n .empty()\n .html(data.attributesHtml);\n\n if (data.shortDescription) {\n $productContainer.find('.description-and-detail .description')\n .removeClass('hidden-xl-down');\n $productContainer.find('.description-and-detail .description .content')\n .empty()\n .html(data.shortDescription);\n } else {\n $productContainer.find('.description-and-detail .description')\n .addClass('hidden-xl-down');\n }\n\n if (data.longDescription) {\n $productContainer.find('.description-and-detail .details')\n .removeClass('hidden-xl-down');\n $productContainer.find('.description-and-detail .details .content')\n .empty()\n .html(data.longDescription);\n } else {\n $productContainer.find('.description-and-detail .details')\n .addClass('hidden-xl-down');\n }\n });\n },\n\n showSpinner: function () {\n $('body').on('product:beforeAddToCart product:beforeAttributeSelect', function () {\n $.spinner().start();\n });\n },\n updateAttribute: function () {\n $('body').on('product:afterAttributeSelect', function (e, response) {\n if ($('.product-detail>.bundle-items').length) {\n response.container.data('pid', response.data.product.id);\n response.container.find('.product-id').text(response.data.product.id);\n } else if ($('.product-set-detail').eq(0)) {\n response.container.data('pid', response.data.product.id);\n response.container.find('.product-id').text(response.data.product.id);\n } else {\n $('.product-id').text(response.data.product.id);\n $('.product-detail:not(\".bundle-item\")').data('pid', response.data.product.id);\n }\n });\n },\n updateAddToCart: function () {\n $('body').on('product:updateAddToCart', function (e, response) {\n // update local add to cart (for sets)\n $('button.add-to-cart', response.$productContainer).attr('disabled',\n (!response.product.readyToOrder || !response.product.available));\n\n var enable = $('.product-availability').toArray().every(function (item) {\n return $(item).data('available') && $(item).data('ready-to-order');\n });\n module.exports.methods.updateAddToCartEnableDisableOtherElements(!enable);\n });\n },\n updateAvailability: function () {\n $('body').on('product:updateAvailability', function (e, response) {\n $('div.availability', response.$productContainer)\n .data('ready-to-order', response.product.readyToOrder)\n .data('available', response.product.available);\n\n $('.availability-msg', response.$productContainer)\n .empty().html(response.message);\n\n if ($('.global-availability').length) {\n var allAvailable = $('.product-availability').toArray()\n .every(function (item) { return $(item).data('available'); });\n\n var allReady = $('.product-availability').toArray()\n .every(function (item) { return $(item).data('ready-to-order'); });\n\n $('.global-availability')\n .data('ready-to-order', allReady)\n .data('available', allAvailable);\n\n $('.global-availability .availability-msg').empty()\n .html(allReady ? response.message : response.resources.info_selectforstock);\n }\n });\n },\n sizeChart: function () {\n $('.size-chart a').on('click', function (e) {\n e.preventDefault();\n var url = $(this).attr('href');\n var $prodSizeChart = $(this).closest('.size-chart').find('.size-chart-collapsible');\n if ($prodSizeChart.is(':empty')) {\n $.ajax({\n url: url,\n type: 'get',\n dataType: 'json',\n success: function (data) {\n $prodSizeChart.append(data.content);\n }\n });\n }\n $prodSizeChart.toggleClass('active');\n });\n\n var $sizeChart = $('.size-chart-collapsible');\n $('body').on('click touchstart', function (e) {\n if ($('.size-chart').has(e.target).length <= 0) {\n $sizeChart.removeClass('active');\n }\n });\n },\n copyProductLink: function () {\n $('body').on('click', '#fa-link', function () {\n event.preventDefault();\n var $temp = $('');\n $('body').append($temp);\n $temp.val($('#shareUrl').val()).select();\n document.execCommand('copy');\n $temp.remove();\n $('.copy-link-message').attr('role', 'alert');\n $('.copy-link-message').removeClass('d-none');\n setTimeout(function () {\n $('.copy-link-message').addClass('d-none');\n }, 3000);\n });\n },\n\n focusChooseBonusProductModal: base.focusChooseBonusProductModal()\n};\n","'use strict';\n\nmodule.exports = function (include) {\n if (typeof include === 'function') {\n include();\n } else if (typeof include === 'object') {\n Object.keys(include).forEach(function (key) {\n if (typeof include[key] === 'function') {\n include[key]();\n }\n });\n }\n};\n","'use strict';\n\nvar processThumbnailActiveStateQuickView = function ($thumbnail, thumbnailIndex) {\n if ($('.js-pdp-carousel').length) {\n $('.js-pdp-carousel').slick('slickGoTo', thumbnailIndex);\n\n // Toggle aria-current state\n $('.js-pdp-carousel-thumb').find('.js-slide-indicator.active').attr('aria-current', false);\n $($thumbnail).attr('aria-current', 'true');\n\n // Toggle active state\n $('.js-pdp-carousel-thumb').find('.js-slide-indicator').removeClass('active');\n $($thumbnail).addClass('active');\n }\n};\n\n/**\n * appends params to a url\n * @param {string} url - Original url\n * @param {Object} params - Parameters to append\n * @returns {string} result url with appended parameters\n */\nfunction appendToUrl(url, params) {\n var newUrl = url;\n newUrl += (newUrl.indexOf('?') !== -1 ? '&' : '?') + Object.keys(params).map(function (key) {\n return key + '=' + encodeURIComponent(params[key]);\n }).join('&');\n\n return newUrl;\n}\n\n/**\n * re-renders the order totals and the number of items in the cart\n * @param {Object} data - AJAX response from the server\n */\nfunction updateCartTotals(data) {\n if (data.resources && data.resources.numberOfItems) {\n $('.number-of-items').empty().append(data.resources.numberOfItems);\n }\n $('.shipping-cost').empty().append(data.totals.totalShippingCost);\n $('.tax-total').empty().append(data.totals.totalTax);\n $('.grand-total').empty().append(data.totals.grandTotal);\n $('.sub-total').empty().append(data.totals.subTotal);\n $('.minicart-quantity').empty().append(data.numItems);\n $('.minicart-link').attr({\n 'aria-label': data.resources.minicartCountOfItems,\n title: data.resources.minicartCountOfItems\n });\n if (data.totals.orderLevelDiscountTotal.value > 0) {\n $('.order-discount').removeClass('hide-order-discount');\n $('.order-discount-total').empty()\n .append('- ' + data.totals.orderLevelDiscountTotal.formatted);\n } else {\n $('.order-discount').addClass('hide-order-discount');\n }\n\n if (data.totals.shippingLevelDiscountTotal.value > 0) {\n $('.shipping-discount').removeClass('hide-shipping-discount');\n $('.shipping-discount-total').empty().append('- ' +\n data.totals.shippingLevelDiscountTotal.formatted);\n } else {\n $('.shipping-discount').addClass('hide-shipping-discount');\n }\n\n data.items.forEach(function (item) {\n if (data.totals.orderLevelDiscountTotal.value > 0) {\n $('.coupons-and-promos').empty().append(data.totals.discountsHtml);\n }\n if (item.renderedPromotions) {\n $('.item-' + item.UUID).empty().append(item.renderedPromotions);\n } else {\n $('.item-' + item.UUID).empty();\n }\n $('.uuid-' + item.UUID + ' .unit-price').empty().append(item.renderedPrice);\n $('.line-item-price-' + item.UUID + ' .unit-price').empty().append(item.renderedPrice);\n $('.item-total-' + item.UUID).empty().append(item.priceTotal.renderedPrice);\n });\n\n if (data.totals.cartType.isBopisOnlyOrder) {\n $('.js-order-pickup-line-item-total').removeClass('d-none');\n $('.js-shipping-line-item-total').addClass('d-none');\n $('.js-shipping-method-selection-cart').addClass('d-none');\n } else if (data.totals.cartType.isMixedCart) {\n $('.js-order-pickup-line-item-total').removeClass('d-none');\n $('.js-shipping-line-item-total').removeClass('d-none');\n $('.js-shipping-method-selection-cart').removeClass('d-none');\n } else {\n $('.js-order-pickup-line-item-total').addClass('d-none');\n $('.js-shipping-line-item-total').removeClass('d-none');\n $('.js-shipping-method-selection-cart').removeClass('d-none');\n }\n}\n\n/**\n * re-renders the approaching discount messages\n * @param {Object} approachingDiscounts - updated approaching discounts for the cart\n */\nfunction updateApproachingDiscounts(approachingDiscounts) {\n var html = '';\n $('.approaching-discounts').empty();\n if (approachingDiscounts.length > 0) {\n approachingDiscounts.forEach(function (item) {\n html += '
'\n + item.discountMsg + '
';\n });\n }\n $('.approaching-discounts').append(html);\n}\n\n/**\n * Checks whether the basket is valid. if invalid displays error message and disables\n * checkout button\n * @param {Object} data - AJAX response from the server\n */\nfunction validateBasket(data) {\n if (data.valid.error) {\n if (data.valid.message) {\n var errorHtml = '
'\n );\n $('.number-of-items').empty().append(data.resources.numberOfItems);\n $('.minicart-quantity').empty().append(data.numItems);\n $('.minicart-link').attr({\n 'aria-label': data.resources.minicartCountOfItems,\n title: data.resources.minicartCountOfItems\n });\n $('.js-minicart').empty();\n $('.js-minicart').modal('hide');\n }\n\n $('.checkout-btn').addClass('disabled');\n } else {\n $('.checkout-btn').removeClass('disabled');\n }\n}\n\n/**\n * re-renders the order totals and the number of items in the cart\n * @param {Object} message - Error message to display\n */\nfunction createErrorNotification(message) {\n var errorHtml = '
' +\n '' + message + '
';\n\n if ($('.cart-error').html().indexOf(errorHtml) === -1) {\n $('.cart-error').html(errorHtml);\n }\n}\n\nmodule.exports = function () {\n $('body').on('click', '.js-remove-product', function (e) {\n e.preventDefault();\n var $this = $(this);\n var productID = $this.data('pid');\n var url = $this.data('action');\n var uuid = $this.data('uuid');\n var urlParams = {\n pid: productID,\n uuid: uuid\n };\n\n url = appendToUrl(url, urlParams);\n\n $.spinner().start();\n\n $('body').trigger('cart:beforeUpdate');\n\n $.ajax({\n url: url,\n type: 'get',\n dataType: 'json',\n success: function (data) {\n if (data.basket.items.length === 0) {\n $('.cart').empty().append('
' +\n '
' +\n '
' + data.basket.resources.emptyCartMsg + '
' +\n '
' +\n '
'\n );\n $('.number-of-items').empty().append(data.basket.resources.numberOfItems);\n $('.minicart-quantity').empty().append(data.basket.numItems);\n $('.minicart-link').attr({\n 'aria-label': data.basket.resources.minicartCountOfItems,\n title: data.basket.resources.minicartCountOfItems\n });\n $('.js-minicart').empty();\n $('.js-minicart').spinner().start();\n $('.js-minicart').modal('hide');\n } else {\n if (data.toBeDeletedUUIDs && data.toBeDeletedUUIDs.length > 0) {\n for (var i = 0; i < data.toBeDeletedUUIDs.length; i++) {\n $('.uuid-' + data.toBeDeletedUUIDs[i]).remove();\n }\n }\n var suuid = $this.parents('.js-card-product-info').data('shipment');\n $('.uuid-' + uuid).remove();\n if ($('.shipment-' + suuid).length < 2) {\n $('.shipment-' + suuid).parents('.js-product-card-container').remove();\n }\n if (!data.basket.hasBonusProduct) {\n $('.bonus-product').remove();\n }\n $('.coupons-and-promos').empty().append(data.basket.totals.discountsHtml);\n updateCartTotals(data.basket);\n updateApproachingDiscounts(data.basket.approachingDiscounts);\n $('body').trigger('setShippingMethodSelection', data.basket);\n validateBasket(data.basket);\n }\n $('body').trigger('cart:update', data);\n $.spinner().stop();\n },\n error: function (err) {\n if (err.responseJSON.redirectUrl) {\n window.location.href = err.responseJSON.redirectUrl;\n } else {\n createErrorNotification(err.responseJSON.errorMessage);\n $.spinner().stop();\n }\n }\n });\n });\n $('.optional-promo').off('click').click(function (e) {\n e.preventDefault();\n $(this).toggleClass('is-expand');\n $('.js-promo-code-form').toggle(200);\n });\n\n $(document).off('click', '.select-bonus-product')\n .on('click', '.select-bonus-product', function () {\n var $choiceOfBonusProduct = $(this).parents('.choice-of-bonus-product');\n var pid = $(this).data('pid');\n var maxPids = $('.choose-bonus-product-dialog').data('total-qty');\n var submittedQty = parseInt($(this).parents('.choice-of-bonus-product').find('.bonus-quantity-select').val(), 10);\n var totalQty = 0;\n $.each($('#chooseBonusProductModal .selected-bonus-products .selected-pid'), function () {\n totalQty += $(this).data('qty');\n });\n totalQty += submittedQty;\n var optionID = $(this).parents('.choice-of-bonus-product').find('.product-option').data('option-id');\n var valueId = $(this).parents('.choice-of-bonus-product').find('.options-select option:selected').data('valueId');\n if (totalQty <= maxPids) {\n var selectedBonusProductHtml = ''\n + '
'\n ;\n $('#chooseBonusProductModal .selected-bonus-products').append(selectedBonusProductHtml);\n $('.pre-cart-products').html(totalQty);\n $('.selected-bonus-products .bonus-summary').removeClass('alert-danger');\n } else {\n $('.selected-bonus-products .bonus-summary').addClass('alert-danger');\n }\n });\n\n $('body').on('shown.bs.modal', '#editProductModal', function () {\n var $pdpCarouselThumbs = $('.js-pdp-carousel-thumb');\n // If the thumbnail slider has less than 6 slides, we need to treat this as if it's not\n // a carousel. So, we have to modify a bunch of aria attributes. ADA-106\n $($pdpCarouselThumbs).on('init', function () {\n if (!$($pdpCarouselThumbs).hasClass('js-pdp-carousel-thumb--more-than-six')) {\n $($pdpCarouselThumbs).removeAttr('role').removeAttr('aria-label');\n $($pdpCarouselThumbs).find('.slick-instructions').attr('aria-hidden', true);\n $($pdpCarouselThumbs).find('.slick-slide').attr('role', 'listitem');\n $($pdpCarouselThumbs).find('.slick-track').attr('role', 'list');\n }\n });\n\n $('.js-pdp-carousel:not(.slick-initialized)').slick();\n $('.js-pdp-carousel-thumb:not(.slick-initialized)').slick();\n\n $('.js-pdp-carousel').on('afterChange', function (e, slick, currentSlide) {\n var $thumbnail = $('.c-image-carousel__thumbnails__item .js-slide-indicator').eq(currentSlide);\n var thumbnailIndex = $($thumbnail).data('slide-to');\n\n processThumbnailActiveStateQuickView($thumbnail, thumbnailIndex);\n });\n });\n\n $('body').on('change', '.js-fulfillment-method', function () {\n $.spinner().start();\n var $this = $(this);\n var $cardprodinfo = $this.parents('.js-card-product-info');\n var url = $cardprodinfo.find('div.js-sourcing-locations').attr('data-action');\n var sourcingLocation = $this.attr('data-value-id');\n var pickupID = $cardprodinfo.find('.js-fulfillment-method-store').attr('data-value-id');\n var ship = $cardprodinfo.find('.js-fulfillment-method-ship').attr('data-value-id');\n\n var uuid = $cardprodinfo.find('.js-remove-product').attr('data-uuid');\n var store = $(this).attr('data-store');\n var odOrderType = $this.attr('data-isodordertype');\n var dataObj = {\n sourcingLocation: sourcingLocation,\n uuid: uuid,\n pickupID: pickupID,\n ship: ship\n };\n $.ajax({\n url: url,\n type: 'post',\n data: dataObj,\n success: function success(data) {\n if (data.sourcingLocationUpdated && data.sourcingLocationUpdated.uuid) {\n $cardprodinfo.find('.js-remove-product').attr('data-uuid', data.sourcingLocationUpdated.uuid);\n $cardprodinfo.removeClass(function (index, className) {\n return (className.match(/(^|\\s)uuid-\\S+/g) || []).join(' ');\n });\n updateCartTotals(data.basket);\n $cardprodinfo.addClass('uuid-' + data.sourcingLocationUpdated.uuid);\n if (odOrderType === 'PIS') {\n $cardprodinfo.prevAll('.row').find('.js-card-product-info-header')\n .find('.js-shipping-fulfillment-method')\n .addClass('d-none');\n $cardprodinfo.prevAll('.row').find('.js-card-product-info-header')\n .find('.js-order-pick-up-fulfillment-method')\n .removeClass('d-none');\n $cardprodinfo.prevAll('.row').find('.js-card-product-info-header')\n .find('.js-order-pick-up-fulfillment-method .js-product-card-store-name')\n .text(store);\n } else {\n $cardprodinfo.prevAll('.row').find('.js-card-product-info-header')\n .find('.js-shipping-fulfillment-method')\n .removeClass('d-none');\n $cardprodinfo.prevAll('.row').find('.js-card-product-info-header')\n .find('.js-order-pick-up-fulfillment-method')\n .addClass('d-none');\n }\n }\n window.location.reload();\n },\n error: function error(err) {\n }\n });\n });\n\n $('.js-promo-code-form').submit(function (e) {\n e.preventDefault();\n $.spinner().start();\n $('.coupon-missing-error').hide();\n $('.coupon-error-message').empty();\n if (!$('.coupon-code-field').val()) {\n // Customization: Added input focus on error\n $('.js-promo-code-form .form-control').addClass('is-invalid').focus();\n $('.js-promo-code-form .form-control').attr('aria-describedby', 'missingCouponCode');\n $('.coupon-missing-error').show();\n $.spinner().stop();\n return false;\n }\n var $form = $('.js-promo-code-form');\n $('.js-promo-code-form .form-control').removeClass('is-invalid');\n $('.coupon-error-message').empty();\n $('body').trigger('promotion:beforeUpdate');\n\n $.ajax({\n url: $form.attr('action'),\n type: 'GET',\n dataType: 'json',\n data: $form.serialize(),\n success: function (data) {\n if (data.error) {\n // Customization: Added input focus on error\n $('.js-promo-code-form .form-control').addClass('is-invalid').focus();\n $('.js-promo-code-form .form-control').attr('aria-describedby', 'invalidCouponCode');\n $('.coupon-error-message').empty().append(data.errorMessage);\n $('body').trigger('promotion:error', data);\n } else {\n $('.coupons-and-promos').empty().append(data.totals.discountsHtml);\n updateCartTotals(data);\n updateApproachingDiscounts(data.approachingDiscounts);\n validateBasket(data);\n $('body').trigger('promotion:success', data);\n if (data.hasBonusProduct) {\n location.reload();\n }\n }\n $('.coupon-code-field').val('');\n $.spinner().stop();\n },\n error: function (err) {\n $('body').trigger('promotion:error', err);\n if (err.responseJSON.redirectUrl) {\n window.location.href = err.responseJSON.redirectUrl;\n } else {\n createErrorNotification(err.errorMessage);\n $.spinner().stop();\n }\n }\n });\n return false;\n });\n\n $('body').on('click', '.js-remove-coupon', function (e) {\n e.preventDefault();\n\n var couponCode = $(this).data('code');\n var uuid = $(this).data('uuid');\n var $deleteConfirmBtn = $('.js-delete-coupon-confirmation-btn');\n var $productToRemoveSpan = $('.coupon-to-remove');\n\n $deleteConfirmBtn.data('uuid', uuid);\n $deleteConfirmBtn.data('code', couponCode);\n\n $productToRemoveSpan.empty().append(couponCode);\n });\n\n $('body').on('click', '.js-delete-coupon-confirmation-btn', function (e) {\n e.preventDefault();\n\n var url = $(this).data('action');\n var uuid = $(this).data('uuid');\n var couponCode = $(this).data('code');\n var urlParams = {\n code: couponCode,\n uuid: uuid\n };\n\n url = appendToUrl(url, urlParams);\n\n $('body > .modal-backdrop').remove();\n\n $.spinner().start();\n $('body').trigger('promotion:beforeUpdate');\n $.ajax({\n url: url,\n type: 'get',\n dataType: 'json',\n success: function (data) {\n $('.coupon-uuid-' + uuid).remove();\n updateCartTotals(data);\n updateApproachingDiscounts(data.approachingDiscounts);\n validateBasket(data);\n $.spinner().stop();\n $('body').trigger('promotion:success', data);\n window.location.reload();\n },\n error: function (err) {\n $('body').trigger('promotion:error', err);\n if (err.responseJSON.redirectUrl) {\n window.location.href = err.responseJSON.redirectUrl;\n } else {\n createErrorNotification(err.responseJSON.errorMessage);\n $.spinner().stop();\n }\n }\n });\n });\n};\n","'use strict';\n\nvar base = require('base/components/clientSideValidation');\n\n/**\n * @param {string} resp - The reCaptcha response data that will be passed to the form\n */\nfunction reCaptchaValidated(resp) {\n if (resp) {\n $('#g-recaptcha-response').removeClass('is-invalid');\n }\n}\n\n// Need to export to window so as to be accessible to the callback\nwindow.reCaptchaValidated = reCaptchaValidated;\n\n/**\n * Validate whole form. Requires `this` to be set to form object\n * @param {jQuery.event} event - Event to be canceled if form is invalid.\n * @returns {boolean} - Flag to indicate if form is valid\n */\nfunction validateForm(event) {\n var valid = true;\n if (this.checkValidity) {\n // safari\n valid = this.checkValidity();\n if (!valid) {\n if (event) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n }\n\n $(this).find('input, select, textarea').each(function () {\n $(this).trigger('invalid', this.validity);\n });\n } else {\n $(this).find('input, select, textarea').removeClass('is-invalid');\n }\n }\n\n return valid;\n}\n\nmodule.exports = $.extend(true, {}, base, {\n // Add form textareas to invalid functionality\n invalid: function () {\n $('form input, form select, form textarea').on('invalid', function (e) {\n e.preventDefault();\n this.setCustomValidity('');\n if (!this.validity.valid) {\n var validationMessage;\n $(this).addClass('is-invalid');\n if (this.validity.patternMismatch && $(this).data('pattern-mismatch')) {\n validationMessage = $(this).data('pattern-mismatch');\n }\n if ((this.validity.rangeOverflow || this.validity.rangeUnderflow)\n && $(this).data('range-error')) {\n validationMessage = $(this).data('range-error');\n }\n if ((this.validity.tooLong || this.validity.tooShort)\n && $(this).data('range-error')) {\n validationMessage = $(this).data('range-error');\n }\n if (this.validity.valueMissing && $(this).data('missing-error')) {\n validationMessage = $(this).data('missing-error');\n }\n\n // IE 11 doesn't have property validity.tooShort, causing a different error message to show when\n // the input has a value that doesn't meet the minimum length. To not change any functionality,\n // new `data-default-error` attribute is used as a default error message if available.\n if (!validationMessage) {\n validationMessage = $(this).data('default-error')\n ? $(this).data('default-error')\n : this.validationMessage;\n }\n $(this).parents('.form-group').find('.invalid-feedback')\n .text(validationMessage);\n } else {\n // Add handling for a valid form\n $(this).removeClass('is-invalid');\n }\n });\n },\n\n submit: function () {\n $('form').on('submit', function (e) {\n return validateForm.call(this, e);\n });\n },\n\n functions: {\n /**\n * Calls validate form on a given form\n *\n * Updated from base to return the validity,\n * and to not render itself pointless by making form into a jQuery object\n *\n * @param {HTML} form - HTML form object\n * @param {Event} event - JS event\n * @returns {boolean} The validity of the form\n */\n validateForm: function (form, event) {\n return validateForm.call(form, event);\n },\n clearForm: base.functions.clearForm\n }\n});\n","'use strict';\n\n/**\n * Initializes the slick slider for the PDP Zoom, sets initial slide based on PDP carousel selection\n */\n\nvar $pdpZoomCarousel = $('.js-zoom-carousel');\n\nfunction adjustZoomCarousel() {\n $('body').on('click', '.js-zoom-button', function (e) {\n $('.js-carousel-item').removeClass('js-zoom-item');\n $(this).prevAll('.js-pdp-carousel').find('.slick-active .js-carousel-item').addClass('js-zoom-item');\n });\n\n $('body').on('hidden.bs.modal', '.js-pdp-carousel-modal', function () {\n // Destroy Zoom Slider, reset thumbnail state and aria attrs\n if ($pdpZoomCarousel.length) {\n $pdpZoomCarousel.slick('unslick');\n $('.js-zoom-thumbnails .js-slide-indicator').removeClass('active').attr('aria-current', 'false');\n }\n });\n\n $($pdpZoomCarousel).on('init afterChange lazyLoaded', function (e) {\n if ($(window).width() >= 1200) {\n const slideHeight = $('.js-zoom-carousel .slick-active .js-pdp-carousel__slider__item__img--zoom').height();\n const modalHeight = $('.js-pdp-img-zoom__body').height();\n const height = modalHeight > slideHeight ? modalHeight : slideHeight;\n $('.js-zoom-carousel, .js-pdp-img-zoom__item').height(height);\n }\n });\n\n // On zoom modal show, determine which image should be set to active\n $('body').on('shown.bs.modal', '.js-pdp-carousel-modal', function () {\n const zoomSlideIndex = $('.js-zoom-item').length ? $('.js-zoom-item').data('carouselIndex') : '0';\n\n if ($pdpZoomCarousel.length) {\n $pdpZoomCarousel.not('.slick-initialized').slick({\n initialSlide: parseInt(zoomSlideIndex, 10)\n });\n }\n\n // detect thumbnail match and apply active state and aria current\n var $activeThumbnail = $('.js-zoom-thumbnails .c-carousel-zoom__thumbnails__item .js-slide-indicator').eq(zoomSlideIndex);\n $($activeThumbnail).addClass('active').attr('aria-current', 'true');\n\n $('.js-zoom-carousel .js-pdp-img-zoom__item').each(function () {\n let itemWithData = $(this).find('.js-pdp-carousel__slider__item__img--zoom');\n let zoomURL = itemWithData.data('zoom-url');\n let magnify = 1.5;\n\n if ($(window).width() >= 1200) {\n $(this).find('.zoomImg').remove();\n $(this).zoom(\n {\n url: zoomURL,\n magnify: magnify,\n on: 'click',\n onZoomIn: function () {\n $(this).addClass('zoomImg--zoomed');\n var zoomImageLink = $(this).prev('.js-pdp-carousel__slider__item__img--zoom').attr('data-href');\n $('.js-zoom-image-link').attr('href', zoomImageLink);\n $('.js-zoom-image-link').removeClass('d-none');\n },\n onZoomOut: function () {\n $(this).removeClass('zoomImg--zoomed');\n $('.js-zoom-image-link').addClass('d-none');\n }\n });\n }\n });\n });\n}\n\nmodule.exports = {\n methods: {\n adjustZoomCarousel: adjustZoomCarousel\n },\n init() {\n const baseModule = this;\n let adjustZoomCarouselScoped = adjustZoomCarousel;\n\n if (baseModule && baseModule.methods && baseModule.methods.adjustZoomCarousel) {\n adjustZoomCarouselScoped = baseModule.methods.adjustZoomCarousel;\n }\n\n adjustZoomCarouselScoped();\n }\n};\n","'use strict';\nvar focusHelper = require('base/components/focus');\n\nfunction validateNamepatch(val) {\n var regex = new RegExp(/^[A-Za-z0-9- ]+$/);\n return regex.test(val);\n}\n\n/**\n * Customization - Add function to validate variation selection\n * @param {jquery} $element - DOM container for product detail options\n * @return {boolean} - returns true if all selections have been made\n */\nfunction validateProductAttributes($element) {\n var isValidate = true;\n // eslint-disable-next-line max-len\n var $productContainer = $element.hasClass('add-to-cart-global') ? $element.parents('.product-set-detail') : $element.parents('.st-product-detail');\n var $attributeContainers = $productContainer.find('.js-product-attribute-item');\n var $firstMissingAttributeContainer;\n var selectableElementClass = 'js-attribute-value';\n var selectedAttribute;\n var errorContainerSelector = '.js-add-to-cart-validation';\n var $errorContainer;\n $attributeContainers.each(function () {\n if (!$(this).hasClass('js-namepatch')) {\n selectedAttribute = $(this).find('.selected, input:checked');\n if (selectedAttribute && selectedAttribute.hasClass(selectableElementClass)) {\n $errorContainer = $(this).find(errorContainerSelector);\n $errorContainer.addClass('d-none');\n } else {\n $firstMissingAttributeContainer = !$firstMissingAttributeContainer ? $(this) : $firstMissingAttributeContainer;\n $errorContainer = $(this).find(errorContainerSelector);\n $errorContainer.removeClass('d-none');\n isValidate = false;\n }\n } else {\n // Only used for B2B & DTI\n var namePatch = $('.js-product-option .js-namepatch:visible input');\n if (namePatch.length > 0 && namePatch.val() !== '' && validateNamepatch(namePatch.val())) {\n $('.js-namepatch').parents('.js-product-option').find('.c-form-element__input--hidden').removeClass('js-product-attribute-item');\n } else {\n $firstMissingAttributeContainer = !$firstMissingAttributeContainer ? $(this) : $firstMissingAttributeContainer;\n $errorContainer = $(this).find(errorContainerSelector);\n $errorContainer.removeClass('d-none');\n isValidate = false;\n }\n }\n });\n\n // scroll to missing attribute\n if ($firstMissingAttributeContainer) {\n $('html, body').animate({\n scrollTop: $firstMissingAttributeContainer.offset().top - 80\n }, 700);\n }\n return isValidate;\n}\n\n/**\n * Retrieves the relevant pid value\n * @param {jquery} $el - DOM container for a given add to cart button\n * @return {string} - value to be used when adding product to cart\n */\nfunction getPidValue($el) {\n var pid;\n\n if ($('#quickViewModal').hasClass('show') && !$('.product-set').length) {\n pid = $($el).closest('.modal-content').find('.product-quickview').data('pid');\n } else if ($('.product-set-detail').length || $('.product-set').length) {\n pid = $($el).closest('.product-detail').find('.product-id').text();\n } else {\n pid = $('.product-detail:not(\".bundle-item\")').data('pid');\n }\n\n return pid;\n}\n\n/**\n * Retrieve contextual quantity selector\n * @param {jquery} $el - DOM container for the relevant quantity\n * @return {jquery} - quantity selector DOM container\n */\nfunction getQuantitySelector($el) {\n var quantitySelected;\n if ($el && $('.set-items').length) {\n quantitySelected = $($el).closest('.product-detail').find('.quantity-select');\n } else if ($el && $('.product-bundle').length) {\n var quantitySelectedModal = $($el).closest('.modal-footer').find('.quantity-select');\n var quantitySelectedPDP = $($el).closest('.bundle-footer').find('.quantity-select');\n if (quantitySelectedModal.val() === undefined) {\n quantitySelected = quantitySelectedPDP;\n } else {\n quantitySelected = quantitySelectedModal;\n }\n } else {\n quantitySelected = $('.quantity-select');\n }\n return quantitySelected;\n}\n\n/**\n * Retrieves the value associated with the Quantity pull-down menu\n * @param {jquery} $el - DOM container for the relevant quantity\n * @return {string} - value found in the quantity input\n */\nfunction getQuantitySelected($el) {\n return getQuantitySelector($el).val();\n}\n\n/**\n * Process the attribute values for an attribute that has image swatches\n *\n * @param {Object} attr - Attribute\n * @param {string} attr.id - Attribute ID\n * @param {Object[]} attr.values - Array of attribute value objects\n * @param {string} attr.values.value - Attribute coded value\n * @param {string} attr.values.url - URL to de/select an attribute value of the product\n * @param {boolean} attr.values.isSelectable - Flag as to whether an attribute value can be\n * selected. If there is no variant that corresponds to a specific combination of attribute\n * values, an attribute may be disabled in the Product Detail Page\n * @param {jQuery} $productContainer - DOM container for a given product\n * @param {Object} msgs - object containing resource messages\n */\nfunction processSwatchValues(attr, $productContainer, msgs) {\n attr.values.forEach(function (attrValue) {\n var $attrValue = $productContainer.find('[data-attr=\"' + attr.id + '\"] [data-attr-value=\"' +\n attrValue.value + '\"]');\n var $swatchButton = $attrValue.parent();\n\n if (attrValue.selected) {\n $attrValue.addClass('selected');\n $attrValue.siblings('.selected-assistive-text').text(msgs.assistiveSelectedText);\n } else {\n $attrValue.removeClass('selected');\n $attrValue.siblings('.selected-assistive-text').empty();\n }\n\n if (attrValue.url) {\n $swatchButton.attr('data-url', attrValue.url);\n } else {\n $swatchButton.removeAttr('data-url');\n }\n\n // Disable if not selectable\n $attrValue.removeClass('selectable unselectable');\n\n $attrValue.addClass(attrValue.selectable ? 'selectable' : 'unselectable');\n });\n}\n\n/**\n * Process attribute values associated with an attribute that does not have image swatches\n *\n * @param {Object} attr - Attribute\n * @param {string} attr.id - Attribute ID\n * @param {Object[]} attr.values - Array of attribute value objects\n * @param {string} attr.values.value - Attribute coded value\n * @param {string} attr.values.url - URL to de/select an attribute value of the product\n * @param {boolean} attr.values.isSelectable - Flag as to whether an attribute value can be\n * selected. If there is no variant that corresponds to a specific combination of attribute\n * values, an attribute may be disabled in the Product Detail Page\n * @param {jQuery} $productContainer - DOM container for a given product\n */\nfunction processNonSwatchValues(attr, $productContainer) {\n var $attr = '[data-attr=\"' + attr.id + '\"]';\n var $defaultOption = $productContainer.find($attr + ' .select-' + attr.id + ' option:first');\n $defaultOption.attr('value', attr.resetUrl);\n\n attr.values.forEach(function (attrValue) {\n var $attrValue = $productContainer\n .find($attr + ' [data-attr-value=\"' + attrValue.value + '\"]');\n $attrValue.attr('value', attrValue.url)\n .removeAttr('disabled');\n\n if (!attrValue.selectable) {\n $attrValue.attr('disabled', true);\n }\n });\n}\n\n/**\n * Routes the handling of attribute processing depending on whether the attribute has image\n * swatches or not\n *\n * @param {Object} attrs - Attribute\n * @param {string} attr.id - Attribute ID\n * @param {jQuery} $productContainer - DOM element for a given product\n * @param {Object} msgs - object containing resource messages\n */\nfunction updateAttrs(attrs, $productContainer, msgs) {\n // Currently, the only attribute type that has image swatches is Color.\n var attrsWithSwatches = ['color'];\n\n attrs.forEach(function (attr) {\n if (attrsWithSwatches.indexOf(attr.id) > -1) {\n processSwatchValues(attr, $productContainer, msgs);\n } else {\n processNonSwatchValues(attr, $productContainer);\n }\n });\n}\n\n/**\n * Updates the availability status in the Product Detail Page\n *\n * @param {Object} response - Ajax response object after an\n * attribute value has been [de]selected\n * @param {jQuery} $productContainer - DOM element for a given product\n */\nfunction updateAvailability(response, $productContainer) {\n var availabilityValue = '';\n var availabilityMessages = response.product.availability.messages;\n if (!response.product.readyToOrder) {\n availabilityValue = '
';\n });\n }\n });\n\n return html;\n}\n\n/**\n * @typedef UpdatedOptionValue\n * @type Object\n * @property {string} id - Option value ID for look up\n * @property {string} url - Updated option value selection URL\n */\n\n/**\n * @typedef OptionSelectionResponse\n * @type Object\n * @property {string} priceHtml - Updated price HTML code\n * @property {Object} options - Updated Options\n * @property {string} options.id - Option ID\n * @property {UpdatedOptionValue[]} options.values - Option values\n */\n\n/**\n * Updates DOM using post-option selection Ajax response\n *\n * @param {OptionSelectionResponse} optionsHtml - Ajax response optionsHtml from selecting a product option\n * @param {jQuery} $productContainer - DOM element for current product\n */\nfunction updateOptions(optionsHtml, $productContainer) {\n // Update options\n $productContainer.find('.product-options').empty().html(optionsHtml);\n}\n\n/**\n * Dynamically creates Bootstrap carousel from response containing images\n * @param {Object[]} imgs - Array of large product images,along with related information\n * @param {jQuery} $productContainer - DOM element for a given product\n */\nfunction createCarousel(imgs, $productContainer) {\n var carousel = $productContainer.find('.carousel');\n $(carousel).carousel('dispose');\n var carouselId = $(carousel).attr('id');\n // eslint-disable-next-line max-len\n $(carousel).empty().append('' + $(carousel).data('prev') + '' + $(carousel).data('next') + '');\n for (var i = 0; i < imgs.length; i++) {\n // eslint-disable-next-line max-len\n $('
').appendTo($(carousel).find('.carousel-inner'));\n $('').appendTo($(carousel).find('.carousel-indicators'));\n }\n $($(carousel).find('.carousel-item')).first().addClass('active');\n $($(carousel).find('.carousel-indicators > li')).first().addClass('active');\n if (imgs.length === 1) {\n $($(carousel).find('.carousel-indicators, a[class^=\"carousel-control-\"]')).detach();\n }\n $(carousel).carousel();\n $($(carousel).find('.carousel-indicators')).attr('aria-hidden', true);\n}\n\n/**\n * Parses JSON from Ajax call made whenever an attribute value is [de]selected\n * @param {Object} response - response from Ajax call\n * @param {Object} response.product - Product object\n * @param {string} response.product.id - Product ID\n * @param {Object[]} response.product.variationAttributes - Product attributes\n * @param {Object[]} response.product.images - Product images\n * @param {boolean} response.product.hasRequiredAttrsSelected - Flag as to whether all required\n * attributes have been selected. Used partially to\n * determine whether the Add to Cart button can be enabled\n * @param {jQuery} $productContainer - DOM element for a given product.\n */\nfunction handleVariantResponse(response, $productContainer) {\n var isChoiceOfBonusProducts =\n $productContainer.parents('.choose-bonus-product-dialog').length > 0;\n var isVaraint;\n if (response.product.variationAttributes) {\n updateAttrs(response.product.variationAttributes, $productContainer, response.resources);\n isVaraint = response.product.productType === 'variant';\n if (isChoiceOfBonusProducts && isVaraint) {\n $productContainer.parent('.bonus-product-item')\n .data('pid', response.product.id);\n\n $productContainer.parent('.bonus-product-item')\n .data('ready-to-order', response.product.readyToOrder);\n }\n }\n\n // Update primary images\n var primaryImageUrls = response.product.images.large;\n createCarousel(primaryImageUrls, $productContainer);\n\n // Update pricing\n if (!isChoiceOfBonusProducts) {\n var $priceSelector = $('.prices .price', $productContainer).length\n ? $('.prices .price', $productContainer)\n : $('.prices .price');\n $priceSelector.replaceWith(response.product.price.html);\n }\n\n // Update promotions\n $productContainer.find('.promotions').empty().html(response.product.promotionsHtml);\n\n updateAvailability(response, $productContainer);\n\n if (isChoiceOfBonusProducts) {\n var $selectButton = $productContainer.find('.select-bonus-product');\n $selectButton.trigger('bonusproduct:updateSelectButton', {\n product: response.product, $productContainer: $productContainer\n });\n } else {\n // Enable \"Add to Cart\" button if all required attributes have been selected\n $('button.add-to-cart, button.add-to-cart-global, button.update-cart-product-global').trigger('product:updateAddToCart', {\n product: response.product, $productContainer: $productContainer\n }).trigger('product:statusUpdate', response.product);\n }\n\n // Update attributes\n $productContainer.find('.main-attributes').empty()\n .html(getAttributesHtml(response.product.attributes));\n}\n\n/**\n * @typespec UpdatedQuantity\n * @type Object\n * @property {boolean} selected - Whether the quantity has been selected\n * @property {string} value - The number of products to purchase\n * @property {string} url - Compiled URL that specifies variation attributes, product ID, options,\n * etc.\n */\n\n/**\n * Updates the quantity DOM elements post Ajax call\n * @param {UpdatedQuantity[]} quantities -\n * @param {jQuery} $productContainer - DOM container for a given product\n */\nfunction updateQuantities(quantities, $productContainer) {\n if ($productContainer.parent('.bonus-product-item').length <= 0) {\n var optionsHtml = quantities.map(function (quantity) {\n var selected = quantity.selected ? ' selected ' : '';\n return '';\n }).join('');\n getQuantitySelector($productContainer).empty().html(optionsHtml);\n }\n}\n\n/**\n * updates the product view when a product attribute is selected or deselected or when\n * changing quantity\n * @param {string} selectedValueUrl - the Url for the selected variation value\n * @param {jQuery} $productContainer - DOM element for current product\n */\nfunction attributeSelect(selectedValueUrl, $productContainer) {\n if (selectedValueUrl) {\n $('body').trigger('product:beforeAttributeSelect',\n { url: selectedValueUrl, container: $productContainer });\n\n $.ajax({\n url: selectedValueUrl,\n method: 'GET',\n success: function (data) {\n handleVariantResponse(data, $productContainer);\n updateOptions(data.product.optionsHtml, $productContainer);\n updateQuantities(data.product.quantities, $productContainer);\n $('body').trigger('product:afterAttributeSelect',\n { data: data, container: $productContainer });\n $.spinner().stop();\n },\n error: function () {\n $.spinner().stop();\n }\n });\n }\n}\n\n/**\n * Retrieves url to use when adding a product to the cart\n *\n * @return {string} - The provided URL to use when adding a product to the cart\n */\nfunction getAddToCartUrl() {\n return $('.add-to-cart-url').val();\n}\n\n/**\n * Parses the html for a modal window\n * @param {string} html - representing the body and footer of the modal window\n *\n * @return {Object} - Object with properties body and footer.\n */\nfunction parseHtml(html) {\n var $html = $('
').append($.parseHTML(html));\n\n var body = $html.find('.choice-of-bonus-product');\n var footer = $html.find('.modal-footer').children();\n\n return { body: body, footer: footer };\n}\n\n/**\n * Retrieves url to use when adding a product to the cart\n *\n * @param {Object} data - data object used to fill in dynamic portions of the html\n */\nfunction chooseBonusProducts(data) {\n $('.modal-body').spinner().start();\n\n if ($('#chooseBonusProductModal').length !== 0) {\n $('#chooseBonusProductModal').remove();\n }\n var bonusUrl;\n if (data.bonusChoiceRuleBased) {\n bonusUrl = data.showProductsUrlRuleBased;\n } else {\n bonusUrl = data.showProductsUrlListBased;\n }\n\n var htmlString = ''\n + '
'\n );\n setTimeout(function () {\n $('.add-to-basket-alert').remove();\n if ($('.cart-page').length) {\n location.reload();\n }\n }, 1500);\n }\n },\n error: function () {\n $.spinner().stop();\n }\n });\n });\n },\n\n getPidValue: getPidValue,\n getQuantitySelected: getQuantitySelected,\n miniCartReportingUrl: miniCartReportingUrl,\n getOptions: getOptions\n};\n","'use strict';\n\nvar detailsBase = require('base/product/detail');\nvar baseModule = require('./base');\n\nvar matchBreakpoints = require('../util/matchBreakpoints');\n\n/**\n * Generates the modal window on the first call.\n * @param {string} id - the id adding to events\n */\nfunction getModalHtmlElement(id) {\n if ($('#' + id).length !== 0) {\n $('#' + id).remove();\n }\n var htmlString = ''\n + '
'\n + '
'\n + ''\n + '
'\n + '
'\n + ' '\n + '
'\n + ''\n + ''\n + '
'\n + '
'\n + '
';\n $('body').append(htmlString);\n}\n\ndetailsBase.fixedATCscroll = function () {\n if (matchBreakpoints(['default', 'sm-up'])) {\n // Calculate the stop from the top of the container, which is always static,\n // to ensure proper positioning when recalculating while the button is sticky\n var $container = $('.js-product-detail-cta-container');\n var $button = $('.prices-add-to-cart-actions');\n var $window = $(window);\n var stop;\n var calculateStop = function () {\n stop = $container.offset().top;\n if ($window.scrollTop() >= stop) {\n $button.addClass('sticky');\n } else {\n $button.removeClass('sticky');\n }\n };\n\n calculateStop();\n\n $(window).on('resize', calculateStop);\n $(document).on('scroll', calculateStop);\n }\n};\n\ndetailsBase.sizeChart = function () {\n var $sizeChart = $('#sizeChartModal');\n $('.size-chart a').on('click', function (e) {\n e.preventDefault();\n var url = $(this).attr('href');\n if (!$sizeChart.length) {\n getModalHtmlElement('sizeChartModal');\n $sizeChart = $('#sizeChartModal');\n $.ajax({\n url: url,\n type: 'get',\n dataType: 'json',\n success: function (data) {\n $sizeChart.attr('aria-modal', 'true').attr('aria-label', 'Size chart');\n $sizeChart.find('.modal-body').html(data.content);\n $sizeChart.find('.modal-header').prepend('
Size Chart
');\n }\n });\n }\n $sizeChart.modal('show');\n $(document).on('click', '#sizeChartModal .close', function () {\n $('.size-chart-link').focus();\n setTimeout(function () {\n $(window).scrollTop($('.size-chart-link').position().top);\n }, 500);\n });\n });\n\n $('body').on('click touchstart', function (e) {\n if ($('.size-chart').has(e.target).length <= 0) {\n $sizeChart.removeClass('active');\n }\n });\n};\n\ndetailsBase.updateAddToCart = function () {\n $('body').on('product:updateAddToCart', function (e, response) {\n // update local add to cart (for sets)\n var selectedAttribute;\n var $attributeContainers = $('.js-product-attribute-item');\n var selectableElementClass = 'js-attribute-value';\n var errorContainerSelector = '.js-add-to-cart-validation';\n var $errorContainer;\n var addToCartTxt;\n var product = response.product;\n var pickup = product.sourcingLocations.pickup || {};\n\n if (pickup.isSelected && !pickup.isPickupInStoreEnabled && product.available) {\n $('button.add-to-cart', response.$productContainer).addClass('disabled').attr('disabled','true');\n $(this).find('.c-product-attribute__header .js-add-to-cart-validation').removeClass('d-none');\n addToCartTxt = $('.st-product-detail__add-to-cart').data('addtocart');\n $('button.add-to-cart').text(addToCartTxt);\n } else if (product.readyToOrder && !product.available) {\n $('button.add-to-cart', response.$productContainer).addClass('disabled').attr('disabled','true');\n $(this).find('.c-product-attribute__header .js-add-to-cart-validation').removeClass('d-none');\n addToCartTxt = $('.st-product-detail__add-to-cart').data('oos');\n $('button.add-to-cart').text(addToCartTxt);\n } else {\n $('button.add-to-cart', response.$productContainer).removeClass('disabled').removeAttr('disabled');\n $(this).find('.c-product-attribute__header .js-add-to-cart-validation').addClass('d-none');\n addToCartTxt = $('.st-product-detail__add-to-cart').data('addtocart');\n $('button.add-to-cart').text(addToCartTxt);\n }\n $attributeContainers.each(function () {\n selectedAttribute = $(this).find('.selected, input:checked');\n if (selectedAttribute && selectedAttribute.hasClass(selectableElementClass)) {\n $errorContainer = $(this).find(errorContainerSelector);\n $errorContainer.addClass('d-none');\n } else {\n $errorContainer = $(this).find(errorContainerSelector);\n $errorContainer.removeClass('d-none');\n }\n });\n });\n};\n\ndetailsBase.updateAvailability = function () {\n $('body').on('product:updateAvailability', function (e, response) {\n $('div.availability', response.$productContainer)\n .data('ready-to-order', response.product.readyToOrder)\n .data('available', response.product.available);\n\n $('.availability-msg', response.$productContainer)\n .empty().html(response.message);\n\n if ($('.global-availability').not('.product-quickview .global-availability').length) {\n var allAvailable = $('.product-availability').toArray()\n .every(function (item) { return $(item).data('available'); });\n\n var allReady = $('.product-availability').toArray()\n .every(function (item) { return $(item).data('ready-to-order'); });\n\n $('.global-availability')\n .data('ready-to-order', allReady)\n .data('available', allAvailable);\n\n $('.global-availability .availability-msg').empty()\n .html(allReady ? response.message : response.resources.info_selectforstock);\n }\n });\n};\n\ndetailsBase.warranty = function () {\n var $warrantyContent = $('#warrantyModal');\n $('.js-warranty-content-link').on('click', function (e) {\n e.preventDefault();\n var url = $(this).attr('href');\n if (!$warrantyContent.length) {\n getModalHtmlElement('warrantyModal');\n $warrantyContent = $('#warrantyModal');\n $.ajax({\n url: url,\n type: 'get',\n dataType: 'json',\n success: function (data) {\n $warrantyContent.find('.modal-body').html(data.content);\n }\n });\n }\n $warrantyContent.modal('show');\n });\n\n $('body').on('click touchstart', function (e) {\n if ($('.js-warranty-content-link').has(e.target).length <= 0) {\n $warrantyContent.removeClass('active');\n }\n });\n};\n\ndetailsBase.availability = baseModule.availability;\ndetailsBase.addToCart = baseModule.addToCart;\ndetailsBase.focusChooseBonusProductModal = baseModule.focusChooseBonusProductModal;\n\nmodule.exports = detailsBase;\n","module.exports = function () {\n $('.js-products-carousel').on('init', function (event, slick) {\n $(this).removeClass('row')\n .closest('.s-slick')\n .removeClass('c-recommendation--slick-uninitialized')\n .find('.st-product-grid__item')\n .removeClass('col-6 col-md-3');\n });\n\n // Set tech icons as unfocusable when slide not active\n $('.js-products-carousel').on('init afterChange', function () {\n $('.slick-slide[aria-hidden=true]').find('.c-product-tile__tech-icons__icon').attr('tabindex', '-1');\n });\n\n $('.js-products-carousel:not(.slick-initialized)').slick({\n slidesToShow: 4,\n responsive: [\n {\n breakpoint: 1025,\n settings: {\n slidesToShow: 3\n }\n },\n {\n breakpoint: 769,\n settings: {\n slidesToShow: 2\n }\n },\n {\n breakpoint: 544,\n settings: {\n slidesToShow: 1\n }\n }\n ]\n });\n\n $('.js-family-carousel').on('init', function (event, slick) {\n $(this).removeClass('row')\n .closest('.s-slick')\n .removeClass('st-product-detail__product-families--slick-uninitialized')\n .find('.st-product-detail__carousel-item')\n .removeClass('col-12 col-md-4');\n });\n\n $('.js-family-carousel').slick({\n slidesToShow: 3,\n responsive: [\n {\n breakpoint: 769,\n settings: {\n slidesToShow: 2\n }\n },\n {\n breakpoint: 544,\n settings: {\n slidesToShow: 1\n }\n }\n ]\n });\n\n $('.js-has-carousel-tab').on('focus click collapsible:toggleVisibility', function () {\n $('.js-products-carousel').slick('refresh');\n });\n\n $(document).on('click', '.c-product-promo__details-link', function () {\n $(this).addClass('details-link-active');\n });\n $(document).on('click', '#contentModal .close', function () {\n $('.details-link-active').trigger('focus');\n $('.c-product-promo__details-link').removeClass('details-link-active');\n });\n};\n","const productBase = require('./base');\n\nvar keyboardAccessibility = require('base/components/keyboardAccessibility');\nconst clientSideValidation = require('app_rws/components/clientSideValidation');\n\n/**\n * Retrieve contextual quantity selector\n * @param {jquery} $el - DOM container for the relevant quantity\n * @return {jquery} - quantity selector DOM container\n */\nfunction getQuantitySelector($el) {\n return $el && $('.set-items').length\n ? $($el).closest('.product-detail').find('.quantity-select')\n : $('.quantity-select');\n}\n\n/**\n * Process attribute values associated with an attribute that does not have image swatches\n *\n * @param {Object} attr - Attribute\n * @param {string} attr.id - Attribute ID\n * @param {Object[]} attr.values - Array of attribute value objects\n * @param {string} attr.values.value - Attribute coded value\n * @param {string} attr.values.url - URL to de/select an attribute value of the product\n * @param {boolean} attr.values.isSelectable - Flag as to whether an attribute value can be\n * selected. If there is no variant that corresponds to a specific combination of attribute\n * values, an attribute may be disabled in the Product Detail Page\n * @param {jQuery} $productContainer - DOM container for a given product\n */\nfunction processNonSwatchValues(attr, $productContainer) {\n var $attr = '[data-attr=\"' + attr.id + '\"]';\n var $defaultOption = $productContainer.find($attr + ' .select-' + attr.id + ' option:first');\n $defaultOption.attr('value', attr.resetUrl);\n\n attr.values.forEach(function (attrValue) {\n var $attrValue = $productContainer\n .find($attr + ' [data-attr-value=\"' + attrValue.value + '\"]');\n $attrValue.attr('value', attrValue.url)\n .removeAttr('disabled');\n\n if (!attrValue.selectable) {\n $attrValue.attr('disabled', true);\n }\n });\n}\n\nfunction setTabIndex() {\n // Set tab index on first focusable variation option, if variation in group is not already selected\n var variationAttribute = $('.js-product-attribute-item');\n $(variationAttribute).each(function () {\n var $this = this;\n var hasSelectedItem = $($this).find('.selected').length > 0;\n if (!(hasSelectedItem)) {\n $($this).find('.selectable:not(.selected)').first().attr('tabindex', '0');\n }\n });\n}\n\n/**\n * Process the attribute values for an attribute that has image swatches\n *\n * @param {Object} attr - Attribute\n * @param {string} attr.id - Attribute ID\n * @param {Object[]} attr.values - Array of attribute value objects\n * @param {string} attr.values.value - Attribute coded value\n * @param {string} attr.values.url - URL to de/select an attribute value of the product\n * @param {boolean} attr.values.isSelectable - Flag as to whether an attribute value can be\n * selected. If there is no variant that corresponds to a specific combination of attribute\n * values, an attribute may be disabled in the Product Detail Page\n * @param {jQuery} $productContainer - DOM container for a given product\n * @param {Object} resources - relevant resource file entries\n */\nfunction processSwatchValues(attr, $productContainer, resources) {\n attr.values.forEach(function (attrValue) {\n var $attrValue = $productContainer.find('[data-attr=\"' + attr.id + '\"] [data-attr-value=\"' +\n attrValue.value + '\"]');\n var $swatchAnchor = $attrValue.parent().attr('data-href') ? $attrValue.parent() : $attrValue;\n\n if (attrValue.selected) {\n $attrValue.addClass('selected');\n $attrValue.attr('aria-checked', 'true');\n $attrValue.attr('tabindex', '0');\n } else {\n $attrValue.removeClass('selected');\n $attrValue.attr('aria-checked', 'false');\n $attrValue.attr('tabindex', '-1');\n }\n\n // Clear out all classes and attributes\n $attrValue.removeClass('selectable unselectable d-none');\n $attrValue.removeAttr('aria-label');\n\n $attrValue.addClass(attrValue.selectable ? 'selectable' : 'unselectable');\n if (!(attrValue.selectable)) {\n $attrValue.attr('aria-label', resources.label_aria_variation_notinstock)\n }\n $attrValue.addClass(attrValue.invalid ? 'd-none' : '');\n\n if (attrValue.url) {\n $swatchAnchor.attr('data-href', attrValue.url);\n } else {\n $swatchAnchor.removeAttr('data-href');\n }\n });\n}\n\n/**\n * Routes the handling of attribute processing depending on whether the attribute has image\n * swatches or not\n *\n * @param {Object} attrs - Attribute\n * @param {string} attr.id - Attribute ID\n * @param {jQuery} $productContainer - DOM element for a given product\n * @param {Object} resources - relevant resource file entries\n */\nfunction updateAttrs(attrs, $productContainer, resources ) {\n // Currently, the only attribute type that has image swatches is Color.\n var attrsWithoutSwatches = [];\n\n attrs.forEach(function (attr) {\n if (attrsWithoutSwatches.indexOf(attr.id) > -1) {\n processNonSwatchValues(attr, $productContainer);\n } else {\n processSwatchValues(attr, $productContainer,resources);\n }\n });\n\n setTabIndex();\n}\n\n/**\n * Updates the availability status in the Product Detail Page\n *\n * @param {Object} response - Ajax response object after an\n * attribute value has been [de]selected\n * @param {jQuery} $productContainer - DOM element for a given product\n */\nfunction updateAvailability(response, $productContainer) {\n var availabilityValue = '';\n var availabilityMessages = response.product.availability.messages;\n if (!response.product.readyToOrder) {\n availabilityValue = '