{"product_id":"whistle-miwok-2160-27-5-2026-km0-2","title":"Whistle Miwok 2160 27.5 2026 km0","description":"\u003cp\u003eCOLORE\n\nANTRACITE\/NERO OPACO\n\nMISURA\n\nS\n\nMECHANICAL SPECIFICATIONS\n\n Maggiori Informazioni \n\n Promo\n Sì \n\n Prezzo Finale\n 524,25 \n\n ×\n\n Validate your login\n\n Please complete your information below to login.\n\n Password\n\n Conferma password\n\n Invia\n\n Accedi\n\n Clienti registrati\n\n Email\n\n Password\n\n Login\n\n Hai dimenticato la password?\n\n Creare un nuovo account?\n\n let socialPopupForm = {\n \"formLoginUrl\":\"\",\n \"forgotFormUrl\":\"\"\n };\n var element = document.getElementById('social-login-popup');\n if (element) {\n var initData = element.getAttribute('data-mage-init');\n var initObject = JSON.parse(initData);\n socialPopupForm=initObject.socialPopupForm;\n }\n\n function appendLoading(blockSelector) {\n var block = document.querySelector(blockSelector);\n if (block) {\n block.style.position = 'relative';\n var loadingDiv = document.createElement('div');\n loadingDiv.className = 'social-login-ajax-loading';\n block.insertBefore(loadingDiv, block.firstChild);\n }\n }\n\n function removeLoading(blockSelector) {\n var block = document.querySelector(blockSelector);\n if (block) {\n block.style.position = '';\n var loadingElement = block.querySelector('.' + 'social-login-ajax-loading');\n if (loadingElement) {\n loadingElement.remove();\n }\n }\n }\n\n function removeMsg(block, messageClass) {\n var blockSelector = document.querySelector(block);\n var messagesToRemove = blockSelector.querySelectorAll('.' + messageClass.replace(\/ \/g, '.'));\n messagesToRemove.forEach(function(message) {\n message.remove();\n });\n }\n\n function _appendMessage (block, message, messageClass) {\n\n var blockSelector = document.querySelector(block);\n var currentMessage = null;\n var messageSection = blockSelector.querySelectorAll(\".\" + messageClass.replace(\/ \/g, '.'));\n\n if (messageSection.length === 0) {\n var divElement = document.createElement('div');\n divElement.className = messageClass;\n blockSelector.insertBefore(divElement, blockSelector.firstChild);\n currentMessage = blockSelector.firstChild;\n } else {\n currentMessage = messageSection[0];\n }\n\n var messageDiv = document.createElement('div');\n messageDiv.textContent = message;\n currentMessage.appendChild(messageDiv);\n }\n\n function addMsg (block, response) {\n\n var message = response.message,\n messageClass = \"\";\n if(response.hasOwnProperty(\"success\")) {\n messageClass = response.success ? \"message-success success message inline-block\" : \"message-error error message inline-block\";\n }\n if(response.hasOwnProperty(\"errors\")) {\n messageClass = !response.errors ? \"message-success success message inline-block\" : \"message-error error message inline-block\";\n }\n if (Array.isArray(message) \u0026amp;\u0026amp; message.length \u0026gt; 0) {\n message.forEach(\n function (msg) {\n _appendMessage(block, msg, messageClass);\n }\n );\n } else if (typeof (message) === 'string') {\n _appendMessage(block, message, messageClass);\n }\n }\n\n function initCustomerLoginForm () {\n return {\n errors: 0,\n hasCaptchaToken: 0,\n showPassword: false,\n displayErrorMessage: false,\n errorMessages: [],\n setErrorMessages (messages) {\n this.errorMessages = [messages];\n this.displayErrorMessage = this.errorMessages.length\n },\n submitForm () {\n this.validate()\n .then(() =\u0026gt; {\n \/\/ do not rename $form, the variable is the expected name in the recaptcha output\n const $form = document.querySelector('#social-form-login');\n if (this.errors === 0) {\n \/\/ $form.submit();\n removeMsg(\".social-login.authentication .social-login-customer-authentication .block-content\", \"message-success success message\");\n removeMsg(\".social-login.authentication .social-login-customer-authentication .block-content\", \"message-error error message\");\n this.dispatchLoginRequest($form);\n }\n })\n .catch((invalid) =\u0026gt; {\n if (invalid.length \u0026gt; 0) {\n invalid[0].focus();\n }\n });\n },\n dispatchLoginRequest: function (form) {\n this.isLoading = true;\n const username = this.$refs['customer-email'].value;\n const password = this.$refs['customer-password'].value;\n const formKey = hyva.getFormKey();\n const bodyFields = {\n 'username': username,\n 'password': password,\n 'formKey': formKey\n };\n appendLoading('.social-login.authentication .social-login-customer-authentication .block-content');\n\n fetch(socialPopupForm.formLoginUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\/json',\n 'X-Requested-With': 'XMLHttpRequest'\n },\n body: JSON.stringify(bodyFields)\n }\n ).then(response =\u0026gt; {\n return response.json();\n }\n ).then(data =\u0026gt; {\n this.isLoading = false;\n addMsg(\".social-login.authentication .social-login-customer-authentication .block-content\", data);\n if (data.errors) {\n\n } else {\n window.location.reload();\n }\n }).catch(error =\u0026gt; {\n\n });\n removeLoading('.social-login.authentication .social-login-customer-authentication .block-content');\n\n }\n }\n }\n\n function forgotBtn () {\n document.querySelector('.social-login.authentication').style.display = 'none';\n document.querySelector('.social-login.forgot').style.display = 'block';\n document.querySelector('.social-login.create').style.display = 'none'; \/\/ Assuming you want to show the createFormContainer\n document.querySelector('.social-login.fake-email').style.display = 'none';\n }\n\n function createBtn () {\n document.querySelector('.social-login.authentication').style.display = 'none';\n document.querySelector('.social-login.forgot').style.display = 'none';\n document.querySelector('.social-login.create').style.display = 'block'; \/\/ Assuming you want to show the createFormContainer\n document.querySelector('.social-login.fake-email').style.display = 'none';\n }\n function showLogin() {\n document.querySelector('.social-login.authentication').style.display = 'block';\n document.querySelector('.social-login.forgot').style.display = 'none';\n document.querySelector('.social-login.create').style.display = 'none'; \/\/ Assuming you want to show the createFormContainer\n document.querySelector('.social-login.fake-email').style.display = 'none';\n }\n\n document.addEventListener(\"DOMContentLoaded\", function () {\n \/\/ Your code here\n function showLogin() {\n document.querySelector('.social-login.authentication').style.display = 'block';\n document.querySelector('.social-login.forgot').style.display = 'none';\n document.querySelector('.social-login.create').style.display = 'none';\n document.querySelector('.social-login.fake-email').style.display = 'none';\n }\n\n document.querySelector('#social-form-password-forget .action.back').addEventListener(\"click\", showLogin);\n document.querySelector('#social-form-create .action.back').addEventListener(\"click\", showLogin);\n });\n\n Creare un nuovo account\n\n Nome \n\n Cognome \n\n Email\n\n Iscriviti alla Newsletter\n\n Tax\/VAT Number\n\n Password\n\n Conferma password\n\n Crea un Account\n\n Indietro\n\n function initCustomerCreateForm () {\n return {\n errors: 0,\n hasCaptchaToken: 0,\n showPassword: false,\n displayErrorMessage: false,\n errorMessages: [],\n setErrorMessages (messages) {\n this.errorMessages = [messages];\n this.displayErrorMessage = this.errorMessages.length\n },\n submitForm () {\n this.validate()\n .then(() =\u0026gt; {\n removeMsg(\".social-login.create .block-content\", \"message-success success message\");\n\n removeMsg(\".social-login.create .block-content\", \"message-error error message\");\n \/\/ do not rename $form, the variable is the expected name in the recaptcha output\n if (this.errors === 0) {\n this.dispatchCreateRequest();\n }\n })\n .catch((invalid) =\u0026gt; {\n if (invalid.length \u0026gt; 0) {\n invalid[0].focus();\n }\n });\n },\n dispatchCreateRequest: function () {\n appendLoading(\".social-login.create .block-content\");\n var createForm = document.querySelector('#social-form-create');\n var formData = new FormData(createForm);\n var parameters = new URLSearchParams(formData).toString();\n\n fetch(socialPopupForm.createFormUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\/x-www-form-urlencoded',\n 'X-Requested-With': 'XMLHttpRequest'\n },\n body: parameters,\n }\n ).then(response =\u0026gt; {\n return response.json();\n }\n ).then(data =\u0026gt; {\n addMsg(\".social-login.forgot .block-content\", data);\n if (data.redirect) {\n window.location.href = data.redirect;\n } else if (data.success) {\n addMsg(\".social-login.create .block-content\", data);\n if (data.url === '' || data.url == null) {\n window.location.reload(true);\n } else {\n window.location.href = data.url;\n }\n } else {\n addMsg(\".social-login.create .block-content\", data);\n }\n\n }).catch(error =\u0026gt; {\n\n });\n removeLoading(\".social-login.create .block-content\")\n }\n\n }\n }\n\n Password dimenticata\n\n Inserisci il tuo indirizzo email qui sotto per ricevere un link di reimpostazione della password.\n\n Email\n\n Invia\n\n Indietro\n\n let inputCaptcha = document.getElementById(\"captcha_user_forgotpassword\");\n inputCaptcha.setAttribute(\"data-validate\", '{\"required\": true}');\n\n document.addEventListener(\"DOMContentLoaded\", function () {\n function reloadCaptcha() {\n let bodyFields = {\n formId: \"user_forgotpassword\"\n };\n fetch('https:\/\/m245.net\/test1\/captcha\/refresh\/', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\/json',\n 'X-Requested-With': 'XMLHttpRequest'\n },\n body: JSON.stringify(bodyFields)\n }\n ).then(response =\u0026gt; {\n return response.json();\n }\n ).then(data =\u0026gt; {\n\n if (data.errors) {\n\n } else {\n document.querySelector(\".captcha-img\").src=data.imgSrc;\n }\n })\n }\n document.querySelector('.action.reload.captcha-reload').addEventListener(\"click\", reloadCaptcha);\n });\n function initCustomerForgotForm () {\n return {\n errors: 0,\n hasCaptchaToken: 0,\n showPassword: false,\n displayErrorMessage: false,\n errorMessages: [],\n setErrorMessages (messages) {\n this.errorMessages = [messages];\n this.displayErrorMessage = this.errorMessages.length\n },\n submitForm () {\n this.validate()\n .then(() =\u0026gt; {\n \/\/ do not rename $form, the variable is the expected name in the recaptcha output\n removeMsg(\".social-login.forgot .block-content\", \"message-success success message\");\n removeMsg(\".social-login.forgot .block-content\", \"message-error error message\");\n if (this.errors === 0) {\n this.dispatchForgotRequest();\n }\n })\n .catch((invalid) =\u0026gt; {\n if (invalid.length \u0026gt; 0) {\n invalid[0].focus();\n }\n });\n },\n dispatchForgotRequest: function () {\n appendLoading('.social-login.forgot .block-content');\n\n var createForm = document.querySelector('#social-form-password-forget');\n var formData = new FormData(createForm);\n var parameters = new URLSearchParams(formData).toString();\n\n fetch(socialPopupForm.forgotFormUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\/x-www-form-urlencoded',\n 'X-Requested-With': 'XMLHttpRequest'\n },\n body: parameters\n }\n ).then(response =\u0026gt; {\n return response.json();\n }\n ).then(data =\u0026gt; {\n addMsg(\".social-login.forgot .block-content\", data);\n if (data.errors) {\n\n } else {\n\n }\n\n }).catch(error =\u0026gt; {\n\n });\n removeLoading('.social-login.forgot .block-content')\n }\n\n }\n }\n\n Oppure accedi con \n\n Registrati con Facebook \n\n function getPopupParams(w, h, l, t) {\n var screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft;\n var screenY = typeof window.screenY !== 'undefined' ? window.screenY : window.screenTop;\n var outerWidth = typeof window.outerWidth !== 'undefined' ? window.outerWidth : document.body.clientWidth;\n var outerHeight = typeof window.outerHeight !== 'undefined' ? window.outerHeight : (document.body.clientHeight - 22);\n var width = w ? w : 500;\n var height = h ? h : 420;\n var left = l ? l : parseInt(screenX + ((outerWidth - width) \/ 2), 10);\n var top = t ? t : parseInt(screenY + ((outerHeight - height) \/ 2.5), 10);\n\n return (\n 'width=' + width +\n ',height=' + height +\n ',left=' + left +\n ',top=' + top\n );\n }\n function clickLoginSocial(login_url, label) {\n var date = new Date();\n var currentTime = date.getTime();\n window.open(login_url + '?' + currentTime, label, getPopupParams());\n }\n\n window.socialAuthenticationPopup = {\"facebook\":{\"label\":\"Facebook\",\"login_url\":\"https:\\\/\\\/www.bici.it\\\/sociallogin\\\/social\\\/login\\\/type\\\/facebook\\\/\",\"url\":\"https:\\\/\\\/www.bici.it\\\/sociallogin\\\/social\\\/login\\\/authen\\\/popup\\\/type\\\/facebook\\\/\",\"key\":\"facebook\",\"btn_key\":\"facebook\"}};\n\n Richiedi informazioni sul prodotto \n I campi contrassegnati dall’asterisco * sono obbligatori\n\n Scrivere il campo Check con questa scritta: \nwww.bici.it\n\n Ho preso visione e accetto l'informativa sulla Privacy Policy * \n\n INVIA LA DOMANDA\n\narrow\n\nQuesta form è protetta da reCAPTCHA - Viene applicata la Google Privacy Policy e Terms of Service.\n\n function initContactForm() {\n return {\n errors: 0,\n hasCaptchaToken: 0,\n submitForm() {\n \/\/ Do not rename $form, the variable is expected to be declared in the recaptcha output\n const $form = document.querySelector('#contact');\n if (!this.hasCaptchaToken) {\n this.errors = 1;\n\n const executeGreCaptcha = () =\u0026gt; {\n grecaptcha.ready(() =\u0026gt; {\n grecaptcha.execute(\n '6LfWkZcpAAAAAINih9s_7PYvwsNsPMVStkJt9SCI',\n {action: 'submit'}\n ).then((token) =\u0026gt; {\n const tokenElement = $form['g\\u002Drecaptcha\\u002Dresponse'];\n\n if (tokenElement \u0026amp;\u0026amp; token \u0026amp;\u0026amp; token.length \u0026gt; 0) {\n $form['g\\u002Drecaptcha\\u002Dresponse'].value = token;\n this.errors = 0;\n this.hasCaptchaToken = 1;\n (this.submitForm || $form.submit).call(this);\n } else {\n this.errors = 1;\n const error = tokenElement\n ? 'ReCaptcha\\u0020validation\\u0020failed,\\u0020please\\u0020try\\u0020again'\n : 'ReCaptcha\\u0020token\\u0020element\\u0020is\\u0020missing.\\u0020Please\\u0020refresh\\u0020the\\u0020page\\u0020and\\u0020trying\\u0020again.';\n if (this.setErrorMessages) {\n this.setErrorMessages([error])\n this.displayErrorMessage = true;\n } else {\n window.dispatchMessages \u0026amp;\u0026amp; window.dispatchMessages([{type: \"error\", text: error}], 5000);\n }\n }\n }).catch(exception =\u0026gt; {\n console.error(exception || `An unknown error occurred during ReCaptcha validation. Are the tokens and the domain correctly set up in the Google console?`)\n });\n })\n }\n\n if (window.grecaptcha) {\n executeGreCaptcha();\n } else {\n const recaptchaUrl = \"https:\/\/www.google.com\/recaptcha\/api.js?render=6LfWkZcpAAAAAINih9s_7PYvwsNsPMVStkJt9SCI\";\n const script = document.createElement('script');\n script.setAttribute('type', 'text\/javascript');\n script.setAttribute('src', recaptchaUrl);\n document.head.appendChild(script);\n\n script.addEventListener(\"load\", executeGreCaptcha, false);\n }\n }\n\n if (this.errors === 0) {\n $form.submit();\n }\n }\n }\n }\n\nServizi Offerti\nVendita in sede e online\n\nServizio officina in sede\n\nAssistenza tecnica\n\nBosch Service - Shimano Service - Mavic Service\n\nSpedizioni e Consegne\nCosto di spedizione in tutta Italia € 10.00\n\nSpedizione GRATUITA per ordini superiori a € 149.00\n\nGestione ordini entro 48 ore lavorative\n\nSpedizioni con corriere BRT\n\nOrari Negozio\nVieni a trovarci:\n\nFratelli Sala - Via Visconti di Modrone 51 - 20846 Macherio (MB)\n\nLun-Ven: 8.30-12.30 \/ 14.00-19.00\n\nSab: 8.30-12.00 \/ 14.30-19.00\n\nOrari Assistenza\nPer qualsiasi informazione non esitare a contattarci:\n\nTel. +39 039.2013567\n\nE-mail. info@bici.it\n\nLun-Ven: 09:00-12:00 \/ 14:30-18:00\n\n Iscriviti alla Newsletter per ricevere\n\n Novità\n\n Sconti\n\n Promozioni\n\n Acconsento al trattamento dei dati per le finalità espresse nell'informativa sul trattamento dei dati personali per finalità di erogazione di servizi web e comunicazioni. \n\n ISCRIVITI \n\narrow\n\nexclamation-circle\n\n function initNewsletterForm() {\n return {\n errors: 0,\n hasCaptchaToken: 0,\n displayErrorMessage: false,\n errorMessages: [],\n setErrorMessages(messages) {\n this.errorMessages = [messages]\n this.displayErrorMessage = this.errorMessages.length\n },\n submitForm() {\n \/\/ Do not rename $form, the variable is expected to be declared in the recaptcha output\n const $form = document.querySelector('#newsletter-validate-detail');\n\n if (this.errors === 0) {\n $form.submit();\n }\n }\n }\n }\n\n Bici.it\n\narrow\nChi Siamo\n\narrow\nContatti\n\narrow\nPagamenti\n\narrow\nSpedizioni\n\narrow\nResi \u0026amp; Garanzie\n\narrow\nApri una richiesta di reso\n\narrow\nPrivacy policy\n\narrow\nCookie policy\n\narrow\nAggiorna le preferenze sui cookie\n\narrow\nTermini \u0026amp; Condizioni\n\n Il tuo bici.it\n\narrow\nAccount\n\narrow\nTracking ordini\n\narrow\nTracking spedizioni\n\narrow\nNewsletter\n\n Categorie Negozio\n\narrow\nBiciclette E Telai\n\narrow\nTrasmissione\n\narrow\nRuote\n\narrow\nComponenti\n\narrow\nCopertoni e Camere\n\narrow\nGruppi\n\narrow\nAccessori\n\narrow\nAbbigliamento\n\narrow\nManutenzione\n\narrow\nIndoor\n\n Marchi\n\narrow\nAtala\n\narrow\nBianchi\n\narrow\nShimano\n\narrow\nMavic\n\narrow\nFizik\n\narrow\nVittoria\n\narrow\nContinental\n\narrow\nLook\n\narrow\nSmp\n\narrow\nBicisupport\n\narrow\nTutti i marchi\n\n Rimani connesso con noi\n\nsocial-facebook\n\nsocial-instagram\n\nPaga sicuro con PayPal\n\n © 2026 - Tutti i diritti riservati. Fratelli Sala s.r.l. - Via Visconti di Modrone, 51, 20846 Macherio MB - P. IVA 00690280961\nSito realizzato da IOHO \n\n 'use strict';\n\n function amBrandsPopup() {\n let cached = false;\n\n const displayError = (error) =\u0026gt; {\n console.error(error);\n typeof window.dispatchMessages !== \"undefined\" \u0026amp;\u0026amp; window.dispatchMessages(\n [{\n type: \"error\",\n text: \"Qualcosa\\u0020\\u00E8\\u0020andato\\u0020storto.\\u0020Riprova\\u0020di\\u0020nuovo.\"\n }], 10000\n );\n };\n\n let letters = [\n \"A\",\"B\",\"C\",\"D\",\n \"E\",\"F\",\"G\",\"H\",\"I\",\"J\",\"K\",\"L\",\"M\",\n \"N\",\"O\",\"P\",\"Q\",\"R\",\"S\",\"T\",\"U\",\"V\",\"W\",\"X\",\"Y\",\"Z\",\"#\"\n ];\n\n return {\n hoverPanelActiveId: null,\n selectedLetter: null,\n items: [],\n letters: [],\n showCount: null,\n showFilter: null,\n showImages: true,\n showSearch: null,\n loading: false,\n label: 'Brands',\n isPopupEnabled: false,\n isTopMenuItemEnabled: false,\n cache: false,\n isMobile: false,\n\n initErrorMessages(errors) {\n const messages = [];\n for (let error in Object.keys(errors)) {\n messages.push({type: 'error', text: errors[error].message});\n }\n typeof window.dispatchMessages !== \"undefined\" \u0026amp;\u0026amp; window.dispatchMessages(messages, 10000);\n },\n\n chooseLetter(letter) {\n this.selectedLetter = this.selectedLetter === letter ? null : letter;\n },\n\n getQuery() {\n return \"query BrandsPopupQuery { ambrandlist(\\n showCount: true\\n displayZero: true\\n ) {\\n all_letters,\\n items {\\n alt\\n brandId\\n cnt\\n image\\n img\\n label\\n letter\\n url\\n }\\n }\\n storeConfig {\\n amshopby_brand_general_brands_brands_popup,\\n amshopby_brand_general_menu_item_label,\\n amshopby_brand_general_brands_page,\\n amshopby_brand_general_topmenu_enabled,\\n amshopby_brand_general_brands_popup_config {\\n show_images,\\n display_zero,\\n filter_display_all,\\n image_width,\\n image_height,\\n show_filter,\\n show_count\\n }\\n } }\" },\n\n \/**\n * Retrieve media url for given path\n *\n * @param path\n * @returns {string}\n *\/\n getMediaUrl(path) {\n let stringToRemove = 'index.php\/';\n let baseUrl = BASE_URL.replace(stringToRemove, '');\n return path.includes(baseUrl) ? path : baseUrl + path;\n },\n\n checkIsMobileResolution() {\n this.isMobile = window.matchMedia('(max-width: 1023px)').matches;\n },\n\n getBrandUrl(url){\n return BASE_URL + url;\n },\n\n init() {\n this.checkIsMobileResolution();\n\n this.$watch('hoverPanelActiveId', (value) =\u0026gt; {\n if (value === 'am-brand') {\n this.sendRequest();\n }\n });\n },\n\n \/**\n * Get brands by letter\n *\n * @param result\n * @returns {{imageWidth: *, isTopMenuItemEnabled: *, showCount: *, displayAll: *, showFilter: *, showImages: *, isPopupEnabled: *, label: *, imageHeight: *, filterDisplayAll: *, brandsPage: *}}\n *\/\n settingsMapper(result) {\n const config = result.data.storeConfig?.amshopby_brand_general_brands_popup_config;\n return {\n showImages: config.show_images,\n showCount: config.show_count,\n showFilter: config.show_filter,\n imageWidth: config.image_width,\n imageHeight: config.image_height,\n filterDisplayAll: config.filter_display_all,\n displayAll: config.display_zero,\n isPopupEnabled: result.data.storeConfig?.amshopby_brand_general_brands_brands_popup,\n label: result.data.storeConfig?.amshopby_brand_general_menu_item_label,\n isTopMenuItemEnabled: result.data.storeConfig?.amshopby_brand_general_topmenu_enabled,\n brandsPage: result.data.storeConfig?.amshopby_brand_general_brands_page,\n }\n },\n\n \/**\n * Send request to graphql\n *\n * @returns {Promise\u003cany\u003e}\n *\/\n async sendRequest() {\n if (this.isMobile) {\n return false;\n }\n\n this.loading = true;\n\n try {\n if (cached) {\n this.loading = false;\n return false;\n }\n\n const response = await fetch('https:\/\/www.bici.it\/graphql', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\/json',\n 'Store': 'default'\n },\n credentials: 'include',\n body: JSON.stringify({query: this.getQuery(), variables: {}})\n });\n const result = await response.json();\n\n if (result?.errors) {\n this.initErrorMessages(result.errors);\n return;\n }\n\n const data = result.data?.ambrandlist;\n const mapper = this.settingsMapper(result);\n\n \/\/ Update component settings\n Object.keys(mapper).forEach(config =\u0026gt; {\n this[config] = mapper[config];\n });\n\n if (!this.filterDisplayAll) {\n letters = data.all_letters.split(',');\n }\n\n const items = data.items.map(brand =\u0026gt; {\n brand.image = brand.img\n ? this.getMediaUrl(brand.img) : (brand.image ? this.getMediaUrl(brand.image) : null);\n return brand;\n });\n\n this.letters = letters.map(letter =\u0026gt; ({\n letter,\n brands: this.getLetterItems(items, letter),\n }));\n\n this.items = this.letters.filter(row =\u0026gt; row.brands?.length \u0026gt; 0);\n\n cached = true;\n } catch (error) {\n displayError(error);\n } finally {\n this.loading = false;\n }\n },\n\n \/**\n * Get brands by letter\n *\n * @param items\n * @param letter\n * @returns {*}\n *\/\n getLetterItems(items, letter) {\n return items.filter(row =\u0026gt; row.letter === letter);\n },\n\n convertHtml(text) {\n let textField = document.createElement(\"textarea\");\n textField.innerHTML = text;\n return textField.value;\n }\n }\n }\n\nTelaio\nAlu formed tubes 27,5\", internal cables, brake calliper on C\/S\n\nForcella\nSuntour XCM RL 27,5\" 100mm with remote lockout\n\nSerie sterzo\nFSA No.57BP-1 1\"1\/8\n\nGuarnitura\nShimano Deore FC-M4100-2 2 Pieces, 36\/26, BSA\n\n {\n \"*\": {\n \"Magento_Ui\/js\/core\/app\": {\"components\":{\"widget-pagodil-product-view-85\":{\"component\":\"IPlusService_XPay\\\/js\\\/view\\\/product\\\/product\",\"displayArea\":\"after_price\",\"config\":{\"template\":\"IPlusService_XPay\\\/product\\\/view\"},\"options\":{\"product_id\":\"85\",\"product_type_id\":\"simple\",\"product_final_price\":526.77,\"product_final_price_in_cent\":\"52677\",\"is_widget_visible\":false,\"is_product_installable\":true,\"pagodil_min_amount\":0,\"pagodil_max_amount\":10000000000,\"pagodil_installments_number\":null,\"pagodil_logo_kind\":\"logo_1\",\"pagodil_info_link\":\"https:\\\/\\\/www.pagodil.it\\\/e-commerce\\\/come-funziona\\\/\",\"pagodil_language\":\"it\"}}}} }\n }\n\n @keyframes newspaperAnimation {\n 0% {\n opacity: 0;\n transform: scale(0) rotate(360deg);\n }\n 100% {\n opacity: 1;\n transform: scale(1) rotate(0deg);\n }\n }\n\n .mfp-newspaper-appear {\n animation: newspaperAnimation 0.5s ease-out forwards;\n }\n\n @keyframes newspaperAnimationOut {\n 0% {\n opacity: 1;\n transform: scale(1) rotate(0deg);\n\n }\n 100% {\n opacity: 0;\n transform: scale(0) rotate(360deg);\n }\n }\n\n .mfp-newspaper-disappear {\n animation: newspaperAnimationOut 0.5s ease-out forwards;\n }\n\n @keyframes zoomInAnimation {\n 0% {\n opacity: 0;\n transform: scale(0);\n }\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n .mfp-zoom-in-appear {\n animation: zoomInAnimation 0.5s ease-out forwards;\n }\n\n @keyframes zoomInAnimationOut {\n 0% {\n opacity: 1;\n transform: scale(1);\n\n }\n 100% {\n\n opacity: 0;\n transform: scale(0);\n }\n }\n\n .mfp-zoom-in-disappear {\n animation: zoomInAnimationOut 0.5s ease-out forwards;\n }\n\n @keyframes slideInLeftAnimation {\n 0% {\n opacity: 0;\n transform: translateX(-10%);\n }\n 100% {\n opacity: 1;\n transform: translateX(0);\n }\n }\n\n .mfp-move-horizontal-appear {\n animation: slideInLeftAnimation 0.5s ease-out forwards;\n }\n\n @keyframes slideInLeftAnimationOut {\n 0% {\n opacity: 1;\n transform: translateX(0);\n\n }\n 100% {\n opacity: 0;\n transform: translateX(20%);\n }\n }\n\n .mfp-move-horizontal-disappear {\n animation: slideInLeftAnimationOut 0.5s ease-out forwards;\n }\n\n @keyframes fadeInDownAnimation {\n 0% {\n opacity: 0;\n transform: translateY(-20%);\n }\n 100% {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n .mfp-move-from-top-appear {\n animation: fadeInDownAnimation 0.5s ease-out forwards;\n }\n\n @keyframes fadeInDownAnimationOut {\n 0% {\n opacity: 1;\n transform: translateY(0);\n\n }\n 100% {\n\n opacity: 0;\n transform: translateY(20%);\n }\n }\n\n .mfp-move-from-top-disappear {\n animation: fadeInDownAnimationOut 0.5s ease-out forwards;\n }\n\n @keyframes flipInYAnimation {\n 0% {\n opacity: 0;\n transform: perspective(400px) rotateY(-90deg);\n }\n 100% {\n opacity: 1;\n transform: perspective(400px) rotateY(0);\n }\n }\n\n .mfp-3d-unfold-appear {\n animation: flipInYAnimation 0.5s ease-out forwards;\n }\n\n @keyframes flipInYAnimationOut {\n 0% {\n opacity: 1;\n transform: perspective(400px) rotateY(0);\n\n }\n 100% {\n\n opacity: 0;\n transform: perspective(400px) rotateY(90deg);\n }\n }\n\n .mfp-3d-unfold-disappear {\n animation: flipInYAnimationOut 0.5s ease-out forwards;\n }\n\n @keyframes zoomOutAnimation {\n 0% {\n opacity: 0;\n transform: scale(2);\n }\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n .mfp-zoom-out-appear {\n animation: zoomOutAnimation 0.5s ease-out forwards;\n }\n\n @keyframes zoomOutAnimationOut {\n 0% {\n opacity: 1;\n transform: scale(1);\n\n }\n 100% {\n\n opacity: 0;\n transform: scale(2);\n }\n }\n\n .mfp-zoom-out-disappear {\n animation: zoomOutAnimationOut 0.5s ease-out forwards;\n }\n\n let popup = ``;\n\n function handleHideModalClickOutSide() {\n let popup_test = document.getElementById(\"popup_test\");\n let test = document.getElementById(\"test\");\n test.style.display = 'block';\n popup_test.append(test);\n popup_test.onclick;\n popup_test.addEventListener('click', function (e) {\n if (e.target === test) {\n const modal = document.querySelector('.wrap-modal-login');\n modal.classList.remove('mfp-move-from-top-appear');\n modal.classList.add('mfp-move-from-top-disappear');\n setTimeout(()=\u0026gt;{\n modal.classList.remove('mfp-move-from-top-disappear');\n hideMyDialog();\n\n },300);\n }\n });\n };\n handleHideModalClickOutSide();\n\n function closeModal () {\n const modal = document.querySelector('.wrap-modal-login');\n modal.classList.remove('mfp-move-from-top-appear');\n modal.classList.add('mfp-move-from-top-disappear');\n setTimeout(()=\u0026gt;{\n modal.classList.remove('mfp-move-from-top-disappear');\n hideMyDialog();\n },300);\n };\n\nwindow.dataLayer = window.dataLayer || [];\n\n(function () {\n \/**\n * @var {[Object]}\n *\/\n const dataToPush = JSON.parse('\\u005B\\u005D');\n\n dataToPush.forEach((dataObject) =\u0026gt; {\n window.dataLayer.push(dataObject);\n });\n})();\n\n (_ =\u0026gt; {\n 'use strict';\n\n const SECTION_KEY = 'amga4_hyva_event_pool';\n const MAGE_CACHE_SECTION_KEY = 'mage-cache-storage';\n const storage = hyva.getBrowserStorage();\n\n window.addEventListener('private-content-loaded', ({ detail }) =\u0026gt; {\n if (!detail?.data) {\n return;\n }\n\n const data = detail?.data[SECTION_KEY];\n if (!data) {\n return;\n }\n\n let eventsPool = data?.events || [];\n if (eventsPool.length) {\n eventsPool.forEach(event =\u0026gt; {\n window.dataLayer.push({ecommerce: null});\n window.dataLayer.push(event);\n });\n\n setTimeout(() =\u0026gt; {\n storage.setItem(\n MAGE_CACHE_SECTION_KEY,\n JSON.stringify({\n ...detail.data,\n [SECTION_KEY]: {}\n })\n );\n }, 0);\n }\n });\n })();\n\n (() =\u0026gt; {\n function src_default(Alpine) {\n Alpine.directive(\"intersect\", Alpine.skipDuringClone((el, { value, expression, modifiers }, { evaluateLater, cleanup }) =\u0026gt; {\n let evaluate = evaluateLater(expression);\n let options = {\n rootMargin: getRootMargin(modifiers),\n threshold: getThreshold(modifiers)\n };\n let observer = new IntersectionObserver((entries) =\u0026gt; {\n entries.forEach((entry) =\u0026gt; {\n if (entry.isIntersecting === (value === \"leave\"))\n return;\n evaluate();\n modifiers.includes(\"once\") \u0026amp;\u0026amp; observer.disconnect();\n });\n }, options);\n observer.observe(el);\n cleanup(() =\u0026gt; {\n observer.disconnect();\n });\n }));\n }\n function getThreshold(modifiers) {\n if (modifiers.includes(\"full\"))\n return 0.99;\n if (modifiers.includes(\"half\"))\n return 0.5;\n if (!modifiers.includes(\"threshold\"))\n return 0;\n let threshold = modifiers[modifiers.indexOf(\"threshold\") + 1];\n if (threshold === \"100\")\n return 1;\n if (threshold === \"0\")\n return 0;\n return Number(`.${threshold}`);\n }\n function getLengthValue(rawValue) {\n let match = rawValue.match(\/^(-?[0-9]+)(px|%)?$\/);\n return match ? match[1] + (match[2] || \"px\") : void 0;\n }\n function getRootMargin(modifiers) {\n const key = \"margin\";\n const fallback = \"0px 0px 0px 0px\";\n const index = modifiers.indexOf(key);\n if (index === -1)\n return fallback;\n let values = [];\n for (let i = 1; i \u0026lt; 5; i++) {\n values.push(getLengthValue(modifiers[index + i] || \"\"));\n }\n values = values.filter((v) =\u0026gt; v !== void 0);\n return values.length ? values.join(\" \").trim() : fallback;\n }\n\n document.addEventListener(\"alpine:init\", () =\u0026gt; {\n window.Alpine.plugin(src_default);\n });\n })();\n\n for (const [selector, deferUntil] of Object.entries({\".product-slider section[x-data]\":\"intersect\",\".product-info [x-data]\":\"intersect\",\"#filters-content [x-data]\":\"intersect\",\"#review_form\":\"intersect\",\"section[x-data^=initRecentlyViewedProductsComponent]\":\"intersect\",\"div[x-data^=initBundleOptions]\":\"intersect\",\"#product_addtocart_form [x-data]\":\"intersect\",\"#notice-cookie-block\":\"idle\"})) {\n document.querySelectorAll(selector).forEach(el =\u0026gt; el.setAttribute('x-defer', `${deferUntil}`));\n }\n\n (function () {\n \"use strict\";\n\n const hasAlpine = new Promise(resolve =\u0026gt; {\n window.addEventListener('alpine:initialized', resolve, {once: true, passive: true});\n });\n\n const hasInteract = new Promise(resolve =\u0026gt; {\n (events =\u0026gt; {\n const onInteract = () =\u0026gt; {\n resolve();\n events.forEach(type =\u0026gt; window.removeEventListener(type, onInteract));\n }\n events.forEach(type =\u0026gt; window.addEventListener(type, onInteract, {once: true, passive: true}))\n })(['touchstart', 'mouseover', 'wheel', 'scroll', 'keydown'])\n });\n\n const onIntersect = (el) =\u0026gt; {\n return new Promise(resolve =\u0026gt; {\n const observer = new IntersectionObserver(entries =\u0026gt; {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n observer.disconnect()\n resolve();\n }\n }\n }, {});\n observer.observe(el);\n });\n }\n\n function runComponent(el) {\n hasAlpine.then(() =\u0026gt; {\n el.removeAttribute('x-ignore');\n queueMicrotask(() =\u0026gt; Alpine.initTree(el));\n });\n }\n\n function initDeferredComponents() {\n document.querySelectorAll('[x-data][x-defer]').forEach(el =\u0026gt; {\n el.setAttribute('x-ignore', '');\n const deferUntil = (el.getAttribute('x-defer') || '').trim();\n switch (deferUntil) {\n case 'interact':\n hasInteract.then(() =\u0026gt; runComponent(el));\n break;\n case 'intersect':\n onIntersect(el).then(() =\u0026gt; runComponent(el))\n break;\n case 'idle':\n window.requestIdleCallback\n ? window.requestIdleCallback(() =\u0026gt; runComponent(el), {timeout: 4000})\n : setTimeout(() =\u0026gt; runComponent(el), 4000);\n break;\n case 'eager':\n runComponent(el);\n break;\n default:\n if (deferUntil.startsWith('event:') \u0026amp;\u0026amp; deferUntil.length \u0026gt; 6) {\n window.addEventListener(deferUntil.substring(6), () =\u0026gt; runComponent(el), {once: true, passive: true});\n }\n }\n });\n }\n\n window.addEventListener('alpine:init', initDeferredComponents, {once: true, passive: true});\n })()\n\n\/\/ x-teleport implementation\n(_ =\u0026gt; {\n document.addEventListener('DOMContentLoaded', event =\u0026gt; {\n \/\/ x-teleport available only for template\n if (!Alpine.version.startsWith('2')) {\n return;\n }\n\n const teleportDirective = (el, expression) =\u0026gt; {\n let target = document.querySelector(expression)\n let clone = el.content.cloneNode(true).firstElementChild;\n\n target.appendChild(clone);\n setTimeout(() =\u0026gt; {\n target.querySelectorAll('[x-data]')\n .forEach(el =\u0026gt; Alpine.initializeComponent(el));\n }, 0)\n };\n\n document.querySelectorAll('template').forEach(el =\u0026gt; {\n if (!el.hasAttribute('x-teleport')) {\n return;\n }\n\n teleportDirective(el, el.getAttribute('x-teleport'));\n });\n\n document.querySelectorAll('[x-data]').forEach(el =\u0026gt; {\n if (el.hasAttribute('x-init')) {\n return;\n }\n\n setTimeout(() =\u0026gt; {\n Alpine.initializeComponent(el);\n }, 0);\n });\n });\n})()\n\n 'use strict';\n function dispatchMessages(messages, hideAfter) {\n const messagesEvent = new CustomEvent(\"messages-loaded\", {\n detail: {\n messages: messages,\n hideAfter: hideAfter\n }\n });\n window.dispatchEvent(messagesEvent);\n }\n\n if (typeof hyva === 'undefined' || (!hyva.getBrowserStorage || !hyva.getCookie || !hyva.setCookie)) {\n console.warn(\"Hyvä helpers are not loaded yet. Make sure they are included before this script\");\n }\n\n (function( hyva, undefined ) {\n\n hyva.initFormKey = () =\u0026gt; {\n const inputSelector = 'input[name=\"form_key\"]',\n formKey = hyva.getFormKey();\n\n Array.from(document.querySelectorAll(inputSelector)).map(function (input) {\n input.value = formKey\n });\n }\n\n hyva.initMessages = () =\u0026gt; {\n try {\n const messages = hyva.getCookie('mage-messages');\n window.mageMessages = messages ? JSON.parse(decodeURIComponent(messages).replace(\/\\+\/g, ' ')) : [];\n\n dispatchMessages(window.mageMessages);\n\n \/\/ empty `mage-messages` cookie\n const skipSetDomain = true;\n hyva.setCookie('mage-messages','', -1, skipSetDomain);\n\n } catch (error) {\n console.warn('Error parsing Cookie Messages:', error);\n }\n }\n\n window.addEventListener('DOMContentLoaded', hyva.initFormKey);\n hyva.alpineInitialized(hyva.initMessages)\n\n }( window.hyva = window.hyva || {} ));\n\n 'use strict';\n {\n const private_content_key = 'mage-cache-storage';\n const private_content_expire_key = 'mage-cache-timeout';\n const private_content_version_key = 'private_content_version';\n const section_data_ids_key = 'section_data_ids';\n const mage_cache_session_id_key = 'mage-cache-sessid';\n const last_visited_store_key = 'last_visited_store';\n\n const ttl = 3600;\n\n if (typeof hyva === 'undefined' || (!hyva.getBrowserStorage || !hyva.getCookie || !hyva.setCookie)) {\n console.warn(\"Hyvä helpers are not loaded yet. Make sure they are included before this script\");\n }\n\n function loadSectionData () {\n const browserStorage = hyva.getBrowserStorage();\n if (!browserStorage) {\n typeof window.dispatchMessages !== \"undefined\" \u0026amp;\u0026amp; window.dispatchMessages(\n [{\n type: \"warning\",\n text: \"Please enable LocalStorage in your browser.\"\n }]\n );\n return;\n }\n try {\n let isInvalid = false;\n\n if (hyva.getCookie(last_visited_store_key) !== CURRENT_STORE_CODE) {\n isInvalid = true;\n }\n hyva.setCookie(last_visited_store_key, CURRENT_STORE_CODE, false, false);\n\n if (!hyva.getCookie(mage_cache_session_id_key)) {\n isInvalid = true;\n browserStorage.removeItem(private_content_key);\n const skipSetDomain = true;\n const days = false;\n hyva.setCookie(mage_cache_session_id_key, true, days, skipSetDomain)\n }\n\n const cookieVersion = hyva.getCookie(private_content_version_key);\n const storageVersion = browserStorage.getItem(private_content_version_key);\n\n if (cookieVersion \u0026amp;\u0026amp; !storageVersion || cookieVersion !== storageVersion) {\n isInvalid = true;\n }\n\n const privateContentExpires = browserStorage.getItem(private_content_expire_key);\n if (privateContentExpires \u0026amp;\u0026amp; new Date(privateContentExpires) \u0026lt; new Date()) {\n browserStorage.removeItem(private_content_key);\n }\n\n if (isInvalid \u0026amp;\u0026amp; cookieVersion) {\n fetchPrivateContent([]);\n } else if (cookieVersion \u0026amp;\u0026amp; storageVersion \u0026amp;\u0026amp; cookieVersion === storageVersion) {\n const privateContent = JSON.parse(browserStorage.getItem(private_content_key));\n if (\n privateContent \u0026amp;\u0026amp;\n privateContentExpires \u0026amp;\u0026amp;\n privateContent.cart \u0026amp;\u0026amp;\n privateContent.customer\n ) {\n dispatchPrivateContent(privateContent);\n } else {\n fetchPrivateContent([]);\n }\n } else {\n if (document.getElementById('default-section-data')) {\n const privateContent = JSON.parse(document.getElementById('default-section-data').innerText.trim());\n dispatchPrivateContent(privateContent);\n } else {\n dispatchPrivateContent({});\n }\n }\n\n } catch (error) {\n console.warn('Error retrieving Private Content:', error);\n }\n }\n\n hyva.alpineInitialized(loadSectionData)\n window.addEventListener('reload-customer-section-data', loadSectionData);\n\n function dispatchPrivateContent(data) {\n const privateContentEvent = new CustomEvent(\"private-content-loaded\", {\n detail: {\n data: data\n }\n });\n window.dispatchEvent(privateContentEvent);\n }\n\n function fetchPrivateContent(sections) {\n fetch(`${BASE_URL}customer\/section\/load\/?sections=${encodeURIComponent(sections.join(','))}`, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application\/json',\n 'X-Requested-With': 'XMLHttpRequest'\n }\n })\n .then(response =\u0026gt; response.ok \u0026amp;\u0026amp; response.json())\n .then(\n data =\u0026gt; {\n if (data) {\n try {\n const browserStorage = hyva.getBrowserStorage();\n\n \/\/ merge new data preserving non-invalidated sections\n const oldSectionData = JSON.parse(browserStorage.getItem(private_content_key) || '{}') || {};\n\n if ((! data.cart || ! data.cart.cartId) \u0026amp;\u0026amp; oldSectionData['checkout-data']) {\n delete oldSectionData['checkout-data'];\n }\n const newSectionData = Object.assign(oldSectionData, data);\n\n dispatchPrivateContent(newSectionData);\n\n \/\/ don't persist messages, they've been dispatched already\n if (newSectionData.messages \u0026amp;\u0026amp; newSectionData.messages.messages ) {\n newSectionData.messages.messages = [];\n }\n\n browserStorage.setItem(private_content_key, JSON.stringify(newSectionData));\n\n const expiresAt = new Date(Date.now() + (ttl * 1000)).toISOString();\n browserStorage.setItem(private_content_expire_key, expiresAt);\n\n const newCookieVersion = hyva.getCookie(private_content_version_key);\n browserStorage.setItem(private_content_version_key, newCookieVersion);\n\n \/\/ We don't need the section_data_ids in Hyvä, but we store them for compatibility\n \/\/ with Luma Fallback. Otherwise, not all sections are loaded in Luma Checkout\n hyva.setCookie(\n section_data_ids_key,\n JSON.stringify(\n Object.keys(data).reduce((sectionDataIds, sectionKey) =\u0026gt; {\n sectionDataIds[sectionKey] = data[sectionKey]['data_id'];\n return sectionDataIds;\n }, {})\n ),\n false,\n true\n );\n } catch (error) {\n console.warn(\"Couldn't store privateContent\", error);\n }\n }\n }\n );\n }\n }\n\n (() =\u0026gt; {\n document.addEventListener('submit', event =\u0026gt; event.target.action = event.target.action.replace('%25uenc%25', hyva.getUenc()));\n })()\n\n (events =\u0026gt; {\n const dispatchUserInteractionEvent = () =\u0026gt; {\n events.forEach(type =\u0026gt; window.removeEventListener(type, dispatchUserInteractionEvent))\n window.dispatchEvent(new Event('init-external-scripts'))\n };\n events.forEach(type =\u0026gt; window.addEventListener(type, dispatchUserInteractionEvent, {once: true, passive: true}))\n })(['touchstart', 'mouseover', 'wheel', 'scroll', 'keydown'])\n\n function initConfigurableOptions(productId, optionConfig) {\n\n function findPathParam(key) {\n const baseUrl = (BASE_URL.substring(0, 2) === '\/\/' ? 'http:' : '') + BASE_URL;\n const baseUrlParts = (new URL(baseUrl)).pathname.replace(\/\\\/$\/, '').split('\/');\n const pathParts = window.location.pathname.split('\/').slice(baseUrlParts.length + 3);\n for (let i = 0; i \u0026lt; pathParts.length; i += 2) {\n if (pathParts[i] === key \u0026amp;\u0026amp; pathParts.length \u0026gt; i) {\n return pathParts[i + 1];\n }\n }\n }\n\n return {\n optionConfig,\n productId,\n itemId: (new URLSearchParams(window.location.search)).get('id') || findPathParam('id'),\n allowedAttributeOptions: [],\n selectedValues: [],\n init() {\n this.findAllowedAttributeOptions();\n this.$nextTick(() =\u0026gt; {\n if (typeof this.optionConfig.defaultValues === 'object') {\n for (const [attributeId, value] of Object.entries(this.optionConfig.defaultValues)) {\n this.changeOption(attributeId, value + '');\n }\n }\n this.preselectQuerystringItems();\n this.preselectLocationHashItems();\n });\n },\n productIndex: 0,\n productIndexForPrice: 0,\n optionIsActive(attributeId, optionId) {\n return !!this.getAllowedAttributeOptions(attributeId).find(\n option =\u0026gt; option.id === optionId\n )\n },\n optionIsEnabled(attributeId, optionId) {\n for (const productId in this.optionConfig.index) {\n if (this.optionConfig.index[productId][attributeId] === optionId) {\n return true;\n }\n }\n return false;\n },\n findSimpleIndex() {\n this.productIndex = this.calculateSimpleIndexForPartialSelection(this.selectedValues);\n this.productIndexForPrice = this.findCheapestProductForPartialSelection(this.selectedValues);\n },\n calculateSimpleIndexForPartialSelection(selectedValues) {\n if (selectedValues.length === 0) return 0;\n return this.findProductIdsForPartialSelection(selectedValues)[0];\n },\n calculateSimpleIndexForFullSelection(selectedValues) {\n if (! this.isFullSelection(selectedValues)) return false;\n\n const productIndexes = this.optionConfig.index;\n return Object.keys(productIndexes).find(productIndex =\u0026gt; {\n const productCandidateOptions = productIndexes[productIndex];\n\n for (const productOption in productCandidateOptions) {\n if (\n !selectedValues[productOption] ||\n selectedValues[productOption] !== productCandidateOptions[productOption]\n ) {\n return false;\n }\n }\n return productIndex;\n });\n },\n findAllowedAttributeOptions() {\n this.allowedAttributeOptions = this.calculateAllowedAttributeOptions(this.selectedValues);\n },\n calculateAllowedAttributeOptions(selectedValues) {\n const allAttributes = this.optionConfig.attributes;\n const allAttributesSorted = Object.values(allAttributes).sort((a,b) =\u0026gt; {\n return a.position - b.position\n });\n\n const newAllowedAttributeOptions = [];\n\n allAttributesSorted.forEach(attribute =\u0026gt; {\n const selectionWithoutAttr = Object.assign({}, this.removeAttrFromSelection(selectedValues, attribute.id));\n const availableIndexes = this.calculateAvailableProductIndexes(selectionWithoutAttr);\n newAllowedAttributeOptions[attribute.id] = allAttributes[attribute.id].options.filter(option =\u0026gt; {\n return !!option.products.find(product =\u0026gt; {\n return availableIndexes.includes(product);\n })\n });\n });\n return newAllowedAttributeOptions;\n },\n calculateAvailableProductIndexes(selectedOptions) {\n if (Object.keys(selectedOptions).length === 0) {\n if (Object.values(this.optionConfig.salable || {}).length) {\n return [].concat.apply([], [].concat.apply([], Object.values(this.optionConfig.salable).map(Object.values))).filter((x, i, a) =\u0026gt; a.indexOf(x) === i)\n }\n return Object.keys(this.optionConfig.index);\n }\n\n const selectedIds = Object.keys(selectedOptions);\n if (Object.values(this.optionConfig.salable || {}).length) {\n const selectedOptionIndexes = selectedIds.map(attrId =\u0026gt; {\n const optionValue = selectedOptions[attrId];\n return this.optionConfig.salable[attrId] \u0026amp;\u0026amp; this.optionConfig.salable[attrId][optionValue] || []\n })\n return selectedOptionIndexes.reduce((acc, optionIndexes) =\u0026gt; {\n return acc.filter(index =\u0026gt; optionIndexes.includes(index));\n });\n } else {\n const productIndexes = this.optionConfig.index;\n return Object.keys(productIndexes).filter(index =\u0026gt; {\n for (const attrId of selectedIds) {\n if (productIndexes[index][attrId] !== `${selectedOptions[attrId]}`) return false\n }\n return true\n });\n }\n },\n findAttributeByOptionId(optionId) {\n for (const attributeId in this.optionConfig.attributes) {\n const attributeOptions = this.optionConfig.attributes[attributeId].options || [];\n if (attributeOptions.find(option =\u0026gt; option.id === optionId)) {\n return attributeId;\n }\n }\n },\n getAllowedAttributeOptions(attributeId) {\n return this.allowedAttributeOptions[attributeId] || []\n },\n getAllAttributeOptions(attributeId) {\n return (\n this.optionConfig.attributes[attributeId] \u0026amp;\u0026amp;\n this.optionConfig.attributes[attributeId].options\n ) || []\n },\n getProductIdsForOption(option) {\n const attributeId = this.findAttributeByOptionId(option.id);\n const allOptions = this.optionConfig.attributes[attributeId];\n const opt = (allOptions \u0026amp;\u0026amp; allOptions.options || []).find(o =\u0026gt; o.id === option.id);\n return opt \u0026amp;\u0026amp; opt.products\n ? opt.products\n : [];\n },\n findProductIdsForPartialSelection(optionSelection) {\n const candidateProducts = Object.values(optionSelection).reduce((candidates, optionId) =\u0026gt; {\n const newCandidates = this.getProductIdsForOption({id: optionId});\n return candidates === null\n ? newCandidates\n : candidates.filter(productId =\u0026gt; newCandidates.includes(productId));\n }, null);\n return candidateProducts || [];\n },\n findCheapestProductForPartialSelection(optionSelection) {\n const candidateProducts = this.findProductIdsForPartialSelection(optionSelection);\n return candidateProducts.reduce((cheapest, simpleIdx) =\u0026gt; {\n \/\/ in the first iteration we start with simpleIdx as the currently cheapest product\n if (! this.optionConfig.optionPrices[cheapest]) return simpleIdx;\n const knownCheapestPrice = this.optionConfig.optionPrices[cheapest].finalPrice.amount;\n return knownCheapestPrice \u0026gt; this.optionConfig.optionPrices[simpleIdx].finalPrice.amount\n ? simpleIdx\n : cheapest;\n }, 0)\n },\n findProductIdToUseForOptionPrice(option) {\n \/\/ try to find a product for a complete selection\n const attributeId = this.findAttributeByOptionId(option.id);\n const optionSelection = Object.assign({}, this.selectedValues, {[attributeId]: option.id});\n const matchingSimpleIndex = this.calculateSimpleIndexForFullSelection(optionSelection);\n \/\/ if there is no complete selection, use the cheapest product for the option\n return matchingSimpleIndex || this.findCheapestProductForPartialSelection(optionSelection);\n },\n getAttributeOptionLabel(option) {\n const optionProduct = this.findProductIdToUseForOptionPrice(option);\n\n if ((! optionProduct) || (optionProduct === this.productIndexForPrice)) {\n return option.label;\n }\n\n const currentPrice = this.getOptionPriceAdjustmentBasePrice();\n\n if (this.optionConfig.optionPrices[optionProduct]) {\n const optionPrice = this.optionConfig.optionPrices[optionProduct].finalPrice.amount;\n if (optionPrice !== currentPrice){\n return option.label + ' ' + hyva.formatPrice(optionPrice - currentPrice, true);\n }\n }\n return option.label;\n },\n getOptionPriceAdjustmentBasePrice() {\n if (this.optionConfig.optionPrices[this.productIndexForPrice]) {\n return this.optionConfig.optionPrices[this.productIndexForPrice].finalPrice.amount\n }\n return this.optionConfig.prices.finalPrice.amount; \/\/ default price if no option selection\n },\n clearOptionIfActive(optionId, value) {\n if (this.selectedValues[optionId] === value) {\n this.blurLabel()\n this.changeOption(optionId, '')\n }\n },\n removeAttrFromSelection(selectedValues, attributeId) {\n attributeId = parseInt(attributeId);\n return selectedValues.reduce((newSelection, val, attr) =\u0026gt; {\n if (attr !== attributeId) {\n newSelection[attr] = val;\n }\n return newSelection;\n }, []);\n },\n changeOption(attributeId, value) {\n if (value === '') {\n this.selectedValues = this.removeAttrFromSelection(this.selectedValues, attributeId)\n } else if (value \u0026amp;\u0026amp; this.getAllowedAttributeOptions(attributeId).find(option =\u0026gt; option.id === value)) {\n this.selectedValues[attributeId] = value;\n }\n this.findSimpleIndex();\n this.findAllowedAttributeOptions();\n this.updatePrices();\n this.updateGallery();\n\n const candidates = this.findProductIdsForPartialSelection(this.selectedValues);\n\n window.dispatchEvent(\n new CustomEvent(\n 'configurable-selection-changed',\n {\n detail: {\n productId: this.productId,\n optionId: attributeId,\n value: value,\n productIndex: this.productIndex,\n selectedValues: this.selectedValues,\n candidates: candidates,\n skuCandidates: Object.values(candidates).map(id =\u0026gt; this.optionConfig.sku[id]),\n }\n }\n )\n );\n },\n calculateIsMinimalPrice() {\n return ! this.isFullSelection(this.selectedValues);\n },\n isFullSelection(selectedValues) {\n return Object.values(selectedValues).length === Object.keys(this.optionConfig.attributes).length;\n },\n updatePrices() {\n const value = this.optionConfig.optionPrices[this.productIndexForPrice] || this.optionConfig.prices;\n window.dispatchEvent(\n new CustomEvent(\n \"update-prices-\" + this.productId,\n {\n detail: Object.assign(\n value,\n { isMinimalPrice: this.calculateIsMinimalPrice() }\n )\n }\n )\n );\n },\n updateGallery () {\n if (this.productIndex) {\n const images = this.optionConfig.images[this.productIndex];\n images \u0026amp;\u0026amp; window.dispatchEvent(new CustomEvent(\n \"update-gallery\",\n { detail: this.sortImagesByPosition(images) }\n ));\n } else {\n window.dispatchEvent(new Event(\"reset-gallery\"));\n }\n },\n sortImagesByPosition(images) {\n return images.sort((x, y) =\u0026gt; {\n return x.position === y.position ? 0 : (parseInt(x.position) \u0026gt; parseInt(y.position) ? 1 : -1)\n });\n },\n onGetCartData(data) {\n if (this.productId === '85') {\n this.preselectCartItems(data);\n }\n },\n preselectCartItems(data) {\n \/\/ pre-select options based on cart data for current (quote) itemId\n const cart = data \u0026amp;\u0026amp; data.cart;\n if (cart \u0026amp;\u0026amp; cart.items) {\n const cartItem = cart.items.find((item) =\u0026gt; {\n return (\n item.item_id === this.itemId\n \u0026amp;\u0026amp; item.product_id === this.productId\n )\n });\n if (cartItem \u0026amp;\u0026amp; cartItem.options \u0026amp;\u0026amp; cartItem.options.length) {\n cartItem.options.map(option =\u0026gt; {\n this.changeOption(option.option_id, option.option_value);\n })\n }\n }\n },\n preselectQuerystringItems() {\n \/\/ pre-select option like ?size=167\n const urlQueryParams = new URLSearchParams(window.location.search.replace('?',''));\n this.preselectItemsBasedOnLocation(attribute =\u0026gt; urlQueryParams.get(attribute.code));\n },\n preselectLocationHashItems() {\n \/\/ pre-select option like #144=167\n const urlHashParams = new URLSearchParams(window.location.hash.replace('#',''));\n this.preselectItemsBasedOnLocation(attribute =\u0026gt; urlHashParams.get(attribute.id));\n },\n preselectItemsBasedOnLocation(getLocationValue) {\n Object.values(this.optionConfig.attributes).map(attribute =\u0026gt; {\n const v = getLocationValue(attribute);\n v \u0026amp;\u0026amp; this.changeOption(attribute.id, v)\n });\n }\n }\n }\n\n function initSwatchOptions(swatchConfig) {\n return {\n swatchConfig,\n getAttributeSwatchData(attributeId) {\n const swatchConfig = Object.assign({}, this.swatchConfig[attributeId]);\n swatchConfig['details'] = JSON.parse(swatchConfig['additional_data']);\n\n return swatchConfig;\n },\n mapSwatchTypeNumberToTypeCode(typeNumber) {\n switch (\"\" + typeNumber) {\n case \"1\":\n return \"color\"\n case \"2\":\n return \"image\"\n case \"3\":\n return \"empty\"\n case \"0\":\n default:\n return \"text\"\n }\n },\n getTypeOfFirstOption(attributeId) {\n for (const optionId in this.swatchConfig[attributeId]) {\n const option = this.swatchConfig[attributeId][optionId];\n if (typeof option.type !== 'undefined') {\n return this.mapSwatchTypeNumberToTypeCode(option.type);\n }\n }\n },\n getVisualSwatchType(attributeId, targetOptionId) {\n \/\/ If a type configuration is present for the given option id, use it\n const config = this.swatchConfig[attributeId];\n if (config[targetOptionId] \u0026amp;\u0026amp; typeof config[targetOptionId].type !== 'undefined') {\n return this.mapSwatchTypeNumberToTypeCode(config[targetOptionId].type);\n }\n\n \/\/ Otherwise - if no config is present for the target option - use the type of the first option\n \/\/ with a type property from the attribute, thus assuming its the same type as the target option.\n \/\/ (This edge case condition can occur on single swatch products if some options are not salable)\n return this.getTypeOfFirstOption(attributeId);\n },\n getSwatchType(attributeId, optionId) {\n \/\/ Deserialize the attribute details the first time they are used\n if (this.swatchConfig[attributeId] \u0026amp;\u0026amp; ! this.swatchConfig[attributeId].details) {\n this.swatchConfig[attributeId] = this.getAttributeSwatchData(attributeId);\n }\n const type = this.swatchConfig[attributeId] \u0026amp;\u0026amp;\n this.swatchConfig[attributeId].details \u0026amp;\u0026amp;\n this.swatchConfig[attributeId].details.swatch_input_type ||\n \"empty\";\n return type === 'visual' ? this.getVisualSwatchType(attributeId, optionId) : type;\n },\n isTextSwatch(attributeId, optionId) {\n return this.getSwatchType(attributeId, optionId) === 'text';\n },\n isVisualSwatch(attributeId, optionId) {\n const type = this.getSwatchType(attributeId, optionId);\n\n return ['image', 'color'].includes(type);\n },\n getSwatchBackgroundStyle(attributeId, optionId) {\n const config = this.getSwatchConfig(attributeId, optionId);\n const type = this.getSwatchType(attributeId, optionId);\n\n if (type === \"color\") {\n return 'background-color:' + config.value;\n } else if (type === \"image\") {\n return \"background: #ffffff url('\" + config.value + \"') no-repeat center\";\n } else {\n return '';\n }\n },\n getSwatchText(attributeId, optionId) {\n const config = this.getSwatchConfig(attributeId, optionId);\n return config.label || config.value || this.getOptionLabelFromOptionConfig(attributeId, optionId);\n },\n getOptionLabelFromOptionConfig(attributeId, optionId) {\n \/\/ Fallback if no value is present in swatchConfig data\n \/\/ Reference issue https:\/\/gitlab.hyva.io\/hyva-themes\/magento2-default-theme\/-\/issues\/190\n const option = this.getAllAttributeOptions(attributeId).filter(option =\u0026gt; option.id === optionId);\n return option \u0026amp;\u0026amp; option[0] \u0026amp;\u0026amp; option[0].label ||'';\n },\n getSwatchConfig(attributeId, optionId) {\n return this.swatchConfig[attributeId] \u0026amp;\u0026amp; this.swatchConfig[attributeId][optionId]\n ? this.swatchConfig[attributeId][optionId]\n : false;\n },\n activeTooltipItem: false,\n tooltipPositionElement: false,\n isTooltipVisible() {\n return this.activeTooltipItem \u0026amp;\u0026amp;\n this.getSwatchConfig(\n this.activeTooltipItem.attribute,\n this.activeTooltipItem.item\n );\n },\n isFirstItemCol() {\n return this.activeTooltipItem.index === 0;\n },\n getTooltipImageStyle(attributeId, optionId) {\n const config = this.getSwatchConfig(attributeId, optionId);\n const type = this.getSwatchType(attributeId, optionId);\n\n if (type === \"color\") {\n return 'background-color:' + config.value + '; width: 110px; height: 90px;';\n } else if (type === \"image\") {\n return \"background: #ffffff url('\" + config.thumb +\n \"') center center no-repeat; width: 110px; height: 90px;\";\n } else {\n return 'display:none';\n }\n },\n getTooltipPosition() {\n return this.tooltipPositionElement ?\n `top: ${this.tooltipPositionElement.offsetTop}px;` +\n `left: ${\n this.tooltipPositionElement.offsetLeft - (\n this.tooltipPositionElement.closest('.snap') \u0026amp;\u0026amp;\n this.tooltipPositionElement.closest('.snap').scrollLeft ||\n 0\n )\n }px;` : ''\n },\n getTooltipLabel() {\n return this.getSwatchConfig(this.activeTooltipItem.attribute, this.activeTooltipItem.item).label\n },\n focusedLabel: false,\n focusLabel(optionId) {\n this.focusedLabel = optionId;\n },\n blurLabel() {\n this.focusedLabel = false;\n },\n showSwatches: false,\n initShowSwatchesIntersect() {\n if ('IntersectionObserver' in window \u0026amp;\u0026amp; !window.scrollY) {\n let io = new IntersectionObserver(\n entries =\u0026gt; {\n entries.map(entry =\u0026gt; {\n if (entry.isIntersecting) {\n this.showSwatches = true;\n io.unobserve(this.$root);\n }\n })\n }\n );\n io.observe(this.$root);\n } else {\n this.showSwatches = true\n }\n }\n }\n }\n\n 'use strict';\n\n const grecaptchaV2LoadCallbacks = [];\n\n function executeRecaptchaLoadCallbacks () {\n while (window.grecaptcha \u0026amp;\u0026amp; grecaptchaV2LoadCallbacks.length \u0026gt; 0) {\n const callback = grecaptchaV2LoadCallbacks.pop();\n typeof callback === 'string' ? window[callback]() : callback();\n }\n }\n\n (function() {\n const loadRecaptchaScript = (event) =\u0026gt; {\n\n if (window.grecaptcha || !event.target.form) {\n return;\n }\n\n const url = 'https:\/\/www.google.com\/recaptcha\/api.js?onload=executeRecaptchaLoadCallbacks';\n const siteKey = '6LfWkZcpAAAAAINih9s_7PYvwsNsPMVStkJt9SCI';\n const script = document.createElement('script');\n script.type = 'text\/javascript';\n script.src = siteKey ? `${url}\u0026amp;render=${siteKey}` : `${url}\u0026amp;render=explicit`;\n script.async = true;\n document.head.appendChild(script);\n\n script.addEventListener('load', () =\u0026gt; {\n Array.from(document.querySelectorAll('button[data-recaptcha-btn]:disabled')).forEach(btn =\u0026gt; btn.removeAttribute('disabled'));\n });\n }\n\n window.forceLoadRecaptchaScript = (form) =\u0026gt; {\n loadRecaptchaScript({target: {form: form || 'dummy'}});\n executeRecaptchaLoadCallbacks();\n };\n\n document.body.addEventListener('input', loadRecaptchaScript, { once: true });\n document.body.addEventListener('focus', loadRecaptchaScript, { once: true });\n\n }());\n\n (function () {\n const reviewComponent = document.querySelector('[x-data=\"initReviewForm()\"]');\n\n reviewComponent?.setAttribute(\n 'x-init',\n `$watch(\n 'errorMessages',\n errorMessages =\u0026gt; {\n errorMessages?.length \u0026gt; 0 \u0026amp;\u0026amp; window.dispatchEvent(new CustomEvent(\"reload-customer-section-data\"))\n }\n );${reviewComponent.getAttribute('x-init') || ''}`\n );\n })();\n\n 'use strict';\n\n function initParentContainerProcessor() {\n const selectors = {\n labelContainer: '.amasty-label-container',\n galleryImg: '#gallery img:not(.amasty-label-image)'\n }\n\n return {\n \/**\n * @param {AmlabelComponent} label\n * @return {void}\n *\/\n process: function (label) {\n this.moveLabelToParent(label);\n this.setParentStyles(label);\n },\n\n \/**\n * @param {AmlabelComponent} label\n * @return {void}\n *\/\n moveLabelToParent: function (label) {\n const newParent = this.getNewParent(label);\n\n if (!newParent) {\n return;\n }\n\n const wrapperElement = newParent.querySelector(label.wrapperClass);\n label.parent = newParent;\n\n \/\/ if same wrapper already exists - move label there\n if (wrapperElement \u0026amp;\u0026amp; !wrapperElement.isSameNode(label.element)) {\n this.insertLabelByPriority(label, wrapperElement);\n } else {\n newParent.prepend(label.element);\n }\n },\n\n \/**\n * @param {AmlabelComponent} label\n * @return {HTMLElement|null}\n *\/\n getNewParent: function (label) {\n \/\/ move label to container from settings\n let newParent = label.element.closest(label.config.path)\n || this.findPreviousNodeExcludingScriptsAndDifferentLabels(label.element)?.querySelector(label.config.path)\n || label.parent.querySelector(label.config.path);\n\n \/\/ Try to find default gallery container on product page if none of the config selectors found\n if (!newParent \u0026amp;\u0026amp; label.config.move !== 1 \u0026amp;\u0026amp; label.config.mode === 'prod') {\n newParent = document.querySelector(selectors.galleryImg)?.parentElement;\n }\n\n return newParent;\n },\n\n \/**\n * @param {HTMLElement} element\n * @return {HTMLElement|null}\n *\/\n findPreviousNodeExcludingScriptsAndDifferentLabels: function (element) {\n const previousElement = element?.previousElementSibling;\n if (!previousElement) {\n return null;\n }\n\n if (previousElement?.tagName !== 'SCRIPT'\n \u0026amp;\u0026amp; !previousElement?.className?.includes?.('amlabel-position-wrapper')\n ) {\n return previousElement;\n }\n\n return this.findPreviousNodeExcludingScriptsAndDifferentLabels(previousElement);\n },\n\n \/**\n * @param {AmlabelComponent} label\n * @param {HTMLElement} wrapperElement\n * @return {void}\n *\/\n insertLabelByPriority: function (label, wrapperElement) {\n const labelContainers = wrapperElement.querySelectorAll(selectors.labelContainer);\n const currentLabelOrder = label.labelContainer.dataset.amlabelOrder;\n let insertAfterContainer = null;\n\n \/\/ find the label behind which we want to insert current one by priority\n labelContainers.forEach(labelContainer =\u0026gt; {\n if (labelContainer.dataset.amlabelOrder \u0026lt; currentLabelOrder) {\n insertAfterContainer = labelContainer;\n }\n });\n\n insertAfterContainer === null\n ? wrapperElement.prepend(label.labelContainer)\n : insertAfterContainer.after(label.labelContainer);\n\n label.element.remove();\n label.element = wrapperElement;\n },\n\n \/**\n * @param {AmlabelComponent} label\n * @return {void}\n *\/\n setParentStyles: function (label) {\n \/* required for child position absolute *\/\n !(label.parent.style.position === 'absolute') \u0026amp;\u0026amp; (label.parent.style.position = 'relative');\n !label.parent.style.width \u0026amp;\u0026amp; label.parent.classList.add('w-fit-content');\n !label.parent.style.marginLeft \u0026amp;\u0026amp; label.parent.classList.add('ml-auto');\n !label.parent.style.marginRight \u0026amp;\u0026amp; label.parent.classList.add('mr-auto');\n },\n }\n }\n\n 'use strict';\n\n function initLabelItemProcessor() {\n \/**\n * @typedef {Object} AmlabelComponent\n *\n * @property {HTMLElement} element Label wrapper element\n * @property {HTMLElement} parent Element where label should be placed\n * @property {boolean} initialized Is label initialized\n * @property {string} wrapperClass Dynamic class for label wrapper to place multiple labels properly by position\n * @property {HTMLElement} labelContainer Container for label text image and tooltip\n * @property {Config} config Label Configuration\n * @property {Object} parentContainerProcessor\n * @property {Object} labelItemProcessor\n *\/\n\n \/**\n * @typedef {Object} AmLabelConfig\n *\n * @property {0|1} alignment Labels Alignment config value (0 - Horizontal| 1 - Vertical)\n * @property {number} label Label Id\n * @property {number} margin Margin between labels config value\n * @property {('cat'|'prod')} mode Label mode\n * @property {number} move\n * @property {number} order Label order\n * @property {string} path The css selector to which label should place\n * @property {string} position Label position\n * @property {string} product Product Id\n * @property {string} redirect_url\n * @property {string} size Label size\n * @property {{\n * backgroundColor: string,\n * color: string,\n * content: string,\n * status: 1|2|3\n * }} tooltip\n *\/\n\n return {\n \/**\n * @param {AmlabelComponent} label\n * @return {void}\n *\/\n process: function(label) {\n this.setLabelSize(label);\n this.setLinkProps(label);\n },\n\n \/**\n * @param {AmlabelComponent} label\n * @return {void}\n *\/\n setLabelSize: function (label) {\n if (!label.config.size) {\n return;\n }\n\n label.labelContainer.style.width = label.parent.offsetWidth * label.config.size \/ 100 + 'px';\n },\n\n \/**\n *\n * @param {Object} $event\n * @param {HTMLElement} tooltipNode\n *\/\n placeTooltip: function($event, tooltipNode) {\n const labelNode = $event.target;\n const labelNodeRect = labelNode.getBoundingClientRect();\n\n tooltipNode.style.bottom = (window.innerHeight - labelNodeRect.top) + 'px';\n tooltipNode.style.left = (labelNodeRect.left + labelNode.offsetWidth \/ 2 ) + 'px';\n },\n\n \/**\n * @param {AmlabelComponent} label\n * @return {void}\n *\/\n setLinkProps: function (label) {\n !!label.config.redirect_url \u0026amp;\u0026amp; label.labelContainer.classList.add('-link', 'cursor-pointer');\n },\n\n \/**\n * @param {Object} $event Click event\n * @param {string} redirectUrl\n *\/\n openLink: function($event, redirectUrl) {\n if (redirectUrl.length === 0) {\n return;\n }\n\n $event.preventDefault();\n window.open(redirectUrl, '_blank');\n }\n }\n }\n\n (function () {\n 'use strict';\n\n const observedEvent = 'amlabel-swatch-product-changed';\n const selectors = {\n swatchOptions: 'div[x-data^=initConfigurable][x-data*=Options]',\n addToCartFormOnListing: 'form.product_addtocart_form',\n galleryImage: '#gallery img:not(.amasty-label-image)',\n labelWrapper: '.amlabel-position-wrapper'\n }\n\n \/**\n * @return {void}\n *\/\n const injectLabelObserver = function () {\n const swatches = document.querySelectorAll(selectors.swatchOptions);\n\n swatches.forEach(swatch =\u0026gt; {\n const isListing = !!swatch.closest(selectors.addToCartFormOnListing);\n const injectedDispatch = `$dispatch('${observedEvent}', `\n + `{productIndex: productIndex, element: $el, optionConfig: optionConfig, isListing: ${isListing ? 1 : 0}})`;\n\n let mutatedInitAttribute = swatch.getAttribute('x-init');\n mutatedInitAttribute += `; $watch('productIndex', (productIndex) =\u0026gt; { ${injectedDispatch} });`;\n swatch.setAttribute('x-init', mutatedInitAttribute);\n });\n }\n\n \/**\n * @param {{\n * productIndex: number,\n * element: HTMLElement,\n * optionConfig: Object,\n * isListing: 0|1\n * }} payload\n * @return {void}\n *\/\n const updateLabel = function ({detail: payload}) {\n const {productIndex, element, optionConfig, isListing} = payload;\n\n if (!productIndex) {\n return;\n }\n\n const reloadUrl = optionConfig.label_reload;\n const headers = {\n \"content-type\": \"application\/x-www-form-urlencoded; charset=UTF-8\",\n };\n const body = new URLSearchParams({\n product_ids: productIndex,\n in_product_list: isListing\n });\n const labelContainerSelector = isListing ? optionConfig.label_category : optionConfig.label_product;\n\n fetch(`${reloadUrl}?${body.toString()}`, {headers: headers, method: 'GET'}).then((response) =\u0026gt; {\n if (response.ok) {\n return response.json();\n }\n }).then((result) =\u0026gt; {\n const labelHtml = result.labels \u0026amp;\u0026amp; result.labels[productIndex];\n let labelContainer = findClosest(element, labelContainerSelector);\n\n \/\/ Try to find default gallery container on product page if none of the config selectors found\n if (labelContainer === null \u0026amp;\u0026amp; isListing === 0) {\n labelContainer = document.querySelector(selectors.galleryImage)?.parentElement;\n }\n\n if (!!labelContainer) {\n !!labelHtml ? replaceLabels(labelContainer, labelHtml) : clearLabelWrapper(labelContainer);\n }\n }).catch((error) =\u0026gt; {\n typeof window.dispatchMessages !== \"undefined\" \u0026amp;\u0026amp; window.dispatchMessages(\n [{\n type: 'error',\n text: error\n }],\n 5000\n );\n });\n }\n\n \/**\n * We use approach from hyva.replaceDomElement method to replace labels and init components\n *\n * @param {HTMLElement} labelContainer\n * @param {string} labelHtml\n * @return {void}\n *\/\n function replaceLabels(labelContainer, labelHtml) {\n const parser = new DOMParser();\n const doc = parser.parseFromString(labelHtml, 'text\/html');\n const nodes = doc.querySelectorAll(selectors.labelWrapper);\n\n if (!nodes) {\n return;\n }\n\n clearLabelWrapper(labelContainer);\n\n \/\/ Extract all the script tags from the new content.\n \/\/ Script tags won't execute when inserted into a dom-element directly,\n \/\/ therefore we need to inject them to the head of the document.\n const tmpScripts = doc.getElementsByTagName('script');\n\n if (tmpScripts.length \u0026gt; 0) {\n \/\/ Push all script tags into an array\n \/\/ (to prevent dom manipulation while iterating over dom nodes)\n const scripts = [...tmpScripts];\n\n \/\/ Iterate over all script tags and duplicate+inject each into the head\n scripts.forEach(script =\u0026gt; {\n let scriptNode = document.createElement('script');\n scriptNode.innerHTML = script.innerHTML;\n document.head.appendChild(scriptNode);\n\n \/\/ Remove the original (non-executing) node from the page\n script.parentNode.removeChild(script);\n });\n }\n\n labelContainer.append(...nodes);\n }\n\n \/**\n * @param {HTMLElement} labelContainer\n * @return {void}\n *\/\n function clearLabelWrapper(labelContainer) {\n labelContainer.querySelectorAll(selectors.labelWrapper).forEach(wrapper =\u0026gt; wrapper.remove());\n }\n\n \/**\n * @param {HTMLElement} currentElement\n * @param {string} selector\n * @return {HTMLElement|null}\n *\/\n function findClosest(currentElement, selector) {\n \/\/ We need to limit parent searches to prevent labels from being associated with the wrong product.\n const maxDepth = 3;\n let counter = 0;\n let closest = null;\n\n while (currentElement.parentNode \u0026amp;\u0026amp; closest === null \u0026amp;\u0026amp; counter \u0026lt; maxDepth) {\n ++counter;\n currentElement = currentElement.parentNode;\n closest = currentElement.querySelector(selector);\n }\n\n return closest;\n }\n\n injectLabelObserver();\n window.addEventListener(observedEvent, updateLabel);\n })();\n\n (function () {\n 'use strict';\n\n const selectors = {\n body: '.catalog-product-view',\n productItem: 'product-item',\n productGalleryImage: '#gallery img'\n }\n\n window.addEventListener('alpine:init', () =\u0026gt; {\n Alpine.directive('amintersect', (el, { expression }, { evaluate, cleanup }) =\u0026gt; {\n const observer = new IntersectionObserver(entries =\u0026gt; {\n entries.forEach(entry =\u0026gt; {\n if (entry.isIntersecting) {\n evaluate(expression);\n observer.disconnect();\n }\n })\n }, { threshold: 0.5 });\n\n observer.observe(getElement());\n\n cleanup(() =\u0026gt; {\n observer.disconnect()\n });\n\n function getElement() {\n return !!document.querySelector(selectors.body) ? getImageProductBlock(el) : getCategoryProductBlock(el);\n }\n\n function getCategoryProductBlock(el) {\n if (el.previousElementSibling === null) {\n return el;\n }\n\n if (!el.previousElementSibling.classList.contains(selectors.productItem)) {\n return getCategoryProductBlock(el.previousElementSibling);\n }\n\n return el.previousElementSibling;\n }\n\n function getImageProductBlock(el) {\n return el.parentNode.querySelector(selectors.productGalleryImage)?.parentNode ?? el.parentNode;\n }\n });\n });\n })();\n\n 'use strict';\n\n (function () {\n const modals = [];\n const excludedFromFocusTrapping = new Set();\n\n function trapFocusInNextModalWithOverlay() {\n for (let idx = modals.length -1; idx \u0026gt;= 0; idx--) {\n const nextOnStack = modals[idx];\n const nextDialogElement = nextOnStack.instance.$refs[nextOnStack.name];\n if (! isOverlayDisabled(nextDialogElement)) {\n hyva.trapFocus(nextDialogElement);\n break;\n }\n }\n }\n\n function focusables(dialogElement) {\n const selector = 'button, [href], input, textarea, select, details, [tabindex]:not([tabindex=\"-1\"])';\n return Array.from(dialogElement.querySelectorAll(selector))\n .filter(el =\u0026gt; !el.hasAttribute('disabled'));\n }\n\n function firstVisible(elements) {\n const a = Array.from(elements);\n for (let i = 0; i \u0026lt; a.length; i++) {\n if (a[i].offsetWidth || a[i].offsetHeight || a[i].getClientRects().length) return a[i];\n }\n return null;\n }\n\n function isInViewport(element) {\n const rect = element \u0026amp;\u0026amp; element.getBoundingClientRect();\n return rect \u0026amp;\u0026amp;\n rect.top \u0026gt;= 0 \u0026amp;\u0026amp;\n rect.left \u0026gt;= 0 \u0026amp;\u0026amp;\n rect.right \u0026lt;= window.innerWidth \u0026amp;\u0026amp;\n rect.bottom \u0026lt;= window.innerHeight;\n }\n\n function setFocusAfterTransition(dialogElement, duration) {\n const nested = Array.from(dialogElement.querySelectorAll('[role=\"dialog\"]'));\n const candidates = Array.from(dialogElement.querySelectorAll('[x-focus-first]'));\n next: for (let candidate of candidates) {\n for (let child of nested) {\n if (child.contains(candidate)) continue next;\n }\n setTimeout(() =\u0026gt; candidate.focus(), 50);\n break;\n }\n window.setTimeout(() =\u0026gt; {\n const focusElement = firstVisible(dialogElement.querySelectorAll('[x-focus-first]')) ||\n focusables(dialogElement)[0] ||\n null;\n focusElement \u0026amp;\u0026amp; isInViewport(focusElement) \u0026amp;\u0026amp; focusElement.focus();\n }, Math.max(1, duration));\n }\n\n function determineTrigger($refs, dialog, trigger) {\n if (typeof trigger === 'undefined' \u0026amp;\u0026amp; typeof dialog === 'object' \u0026amp;\u0026amp; dialog.target instanceof HTMLElement) {\n return dialog.target;\n }\n if (typeof dialog === 'string' \u0026amp;\u0026amp; typeof trigger === 'object' \u0026amp;\u0026amp; trigger.target instanceof HTMLElement) {\n return trigger.target;\n }\n if (typeof trigger === 'string') {\n try {\n return $refs[trigger] || document.querySelector(trigger)\n } catch (e) {}\n }\n if (trigger instanceof Element) {\n return trigger;\n }\n\n return null;\n }\n\n function isOverlayDisabled(dialog) {\n return dialog \u0026amp;\u0026amp; dialog.hasAttribute('x-no-overlay')\n }\n\n function areRemainingModalsWithoutOverlay(modals)\n {\n const overflowDisabled = modals.map(modal =\u0026gt; modal.instance.$refs[modal.name]).filter(isOverlayDisabled);\n\n return overflowDisabled.length === modals.length;\n }\n\n window.hyva.modal = function(options) {\n\n const config = Object.assign({\n dialog: 'dialog', duration: 300, transitionEnter: 'transition ease-out duration-300',\n transitionEnterStart: 'opacity-0',\n transitionEnterEnd: 'opacity-100',\n transitionLeave: 'transition ease-in duration-300',\n transitionLeaveStart: 'opacity-100',\n transitionLeaveEnd: 'opacity-0',\n }, options);\n let lastHide = 0;\n\n return {\n opened: {},\n show(dialog, trigger) {\n const focusTargetAfterHide = determineTrigger(this.$refs, dialog, trigger);\n const name = typeof dialog === 'string' ? dialog : config.dialog;\n const dialogElement = this.$refs[name];\n if (! dialogElement) {\n return;\n }\n const useOverlay = ! dialogElement.hasAttribute('x-no-overlay');\n\n dialogElement.scrollTop = 0;\n\n if (this.opened[name]) {\n return;\n }\n\n if (focusTargetAfterHide) {\n focusTargetAfterHide.setAttribute('aria-expanded', 'true');\n }\n\n this.opened[name] = true;\n useOverlay \u0026amp;\u0026amp; this.$nextTick(() =\u0026gt; hyva.trapFocus(dialogElement));\n setFocusAfterTransition(dialogElement, config.duration);\n\n const frame = {name, instance: this, focusTarget: focusTargetAfterHide, time: Date.now()};\n\n modals.push(frame);\n if (useOverlay) {\n document.body.classList.add('overflow-hidden');\n }\n return new Promise(resolve =\u0026gt; frame.resolve = resolve);\n },\n cancel() {\n this.hide(false);\n },\n ok() {\n this.hide(true);\n },\n hide(value) {\n if (Date.now() - lastHide \u0026lt; config.duration) {\n return;\n }\n lastHide = Date.now();\n\n const modal = modals.pop() || {};\n const name = modal.name;\n this.opened[name] = false;\n hyva.releaseFocus(modal.instance.$refs[modal.name])\n trapFocusInNextModalWithOverlay();\n\n const nextFocusAfterHide = modal.focusTarget;\n nextFocusAfterHide \u0026amp;\u0026amp; setTimeout(() =\u0026gt; {\n nextFocusAfterHide.setAttribute('aria-expanded', 'false');\n nextFocusAfterHide.focus()\n }, config.duration);\n\n if (modals.length === 0 || areRemainingModalsWithoutOverlay(modals)) {\n document.body.classList.remove('overflow-hidden');\n }\n\n modal.resolve(value);\n },\n overlay(dialog) {\n const name = typeof dialog === 'string' ? dialog : config.dialog;\n return {\n ['x-show']() {\n return this.opened[name]\n },\n ['x-transition:enter']: config.transitionEnter,\n ['x-transition:enter-start']: config.transitionEnterStart,\n ['x-transition:enter-end']: config.transitionEnterEnd,\n ['x-transition:leave']: config.transitionLeave,\n ['x-transition:leave-start']: config.transitionLeaveStart,\n ['x-transition:leave-end']: config.transitionLeaveEnd,\n ['@hyva-modal-show.window'](event) {\n event.detail \u0026amp;\u0026amp; event.detail.dialog === name \u0026amp;\u0026amp; this.show(name, event.detail.focusAfterHide)\n }\n };\n }\n };\n }\n\n window.hyva.modal.peek = () =\u0026gt; modals.length \u0026gt; 0 \u0026amp;\u0026amp; modals[modals.length -1]\n\n window.hyva.modal.pop = function () {\n if (modals.length \u0026gt; 0) {\n const modal = modals[modals.length -1];\n modal.instance.hide();\n }\n }\n\n window.hyva.modal.excludeSelectorsFromFocusTrap = function (selectors) {\n typeof selectors === 'string' || selectors instanceof String\n ? excludedFromFocusTrapping.add(selectors)\n : selectors.map(selector =\u0026gt; excludedFromFocusTrapping.add(selector));\n }\n\n window.hyva.modal.eventListeners = {\n keydown: event =\u0026gt; {\n if (event.key === 'Escape') {\n window.hyva.modal.pop();\n }\n },\n click: event =\u0026gt; {\n if (modals.length \u0026gt; 0) {\n const modal = modals[modals.length -1];\n const dialog = modal.instance.$refs[modal.name];\n if (modal.time + 50 \u0026lt; Date.now() \u0026amp;\u0026amp; \/\/ if last click processing is more than 50ms ago\n ! isOverlayDisabled(dialog) \u0026amp;\u0026amp; \/\/ if dialog has overlay\n ! dialog.contains(event.target)) { \/\/ if click is outside of dialog\n modal.instance.hide();\n }\n }\n }\n };\n\n document.addEventListener('keydown', window.hyva.modal.eventListeners.keydown);\n\n document.addEventListener('click', window.hyva.modal.eventListeners.click);\n })();\n\n {\"messages\":[],\"customer\":[],\"compare-products\":[],\"last-ordered-items\":[],\"cart\":[],\"directory-data\":{\"IT\":{\"name\":\"Italia\",\"regions\":{\"811\":{\"code\":\"AG\",\"name\":\"Agrigento\"},\"812\":{\"code\":\"AL\",\"name\":\"Alessandria\"},\"813\":{\"code\":\"AN\",\"name\":\"Ancona\"},\"814\":{\"code\":\"AO\",\"name\":\"Aosta\"},\"816\":{\"code\":\"AR\",\"name\":\"Arezzo\"},\"817\":{\"code\":\"AP\",\"name\":\"Ascoli-Piceno\"},\"818\":{\"code\":\"AT\",\"name\":\"Asti\"},\"819\":{\"code\":\"AV\",\"name\":\"Avellino\"},\"820\":{\"code\":\"BA\",\"name\":\"Bari\"},\"821\":{\"code\":\"BT\",\"name\":\"Barletta-Andria-Trani\"},\"822\":{\"code\":\"BL\",\"name\":\"Belluno\"},\"823\":{\"code\":\"BN\",\"name\":\"Benevento\"},\"824\":{\"code\":\"BG\",\"name\":\"Bergamo\"},\"825\":{\"code\":\"BI\",\"name\":\"Biella\"},\"826\":{\"code\":\"BO\",\"name\":\"Bologna\"},\"827\":{\"code\":\"BZ\",\"name\":\"Bolzano\"},\"828\":{\"code\":\"BS\",\"name\":\"Brescia\"},\"829\":{\"code\":\"BR\",\"name\":\"Brindisi\"},\"830\":{\"code\":\"CA\",\"name\":\"Cagliari\"},\"831\":{\"code\":\"CL\",\"name\":\"Caltanissetta\"},\"832\":{\"code\":\"CB\",\"name\":\"Campobasso\"},\"833\":{\"code\":\"CI\",\"name\":\"Carbonia Iglesias\"},\"834\":{\"code\":\"CE\",\"name\":\"Caserta\"},\"835\":{\"code\":\"CT\",\"name\":\"Catania\"},\"836\":{\"code\":\"CZ\",\"name\":\"Catanzaro\"},\"837\":{\"code\":\"CH\",\"name\":\"Chieti\"},\"838\":{\"code\":\"CO\",\"name\":\"Como\"},\"839\":{\"code\":\"CS\",\"name\":\"Cosenza\"},\"840\":{\"code\":\"CR\",\"name\":\"Cremona\"},\"841\":{\"code\":\"KR\",\"name\":\"Crotone\"},\"842\":{\"code\":\"CN\",\"name\":\"Cuneo\"},\"843\":{\"code\":\"EN\",\"name\":\"Enna\"},\"844\":{\"code\":\"FM\",\"name\":\"Fermo\"},\"845\":{\"code\":\"FE\",\"name\":\"Ferrara\"},\"846\":{\"code\":\"FI\",\"name\":\"Firenze\"},\"847\":{\"code\":\"FG\",\"name\":\"Foggia\"},\"848\":{\"code\":\"FC\",\"name\":\"Forli-Cesena\"},\"849\":{\"code\":\"FR\",\"name\":\"Frosinone\"},\"850\":{\"code\":\"GE\",\"name\":\"Genova\"},\"851\":{\"code\":\"GO\",\"name\":\"Gorizia\"},\"852\":{\"code\":\"GR\",\"name\":\"Grosseto\"},\"853\":{\"code\":\"IM\",\"name\":\"Imperia\"},\"854\":{\"code\":\"IS\",\"name\":\"Isernia\"},\"815\":{\"code\":\"AQ\",\"name\":\"L'Aquila\"},\"855\":{\"code\":\"SP\",\"name\":\"La-Spezia\"},\"856\":{\"code\":\"LT\",\"name\":\"Latina\"},\"857\":{\"code\":\"LE\",\"name\":\"Lecce\"},\"858\":{\"code\":\"LC\",\"name\":\"Lecco\"},\"859\":{\"code\":\"LI\",\"name\":\"Livorno\"},\"860\":{\"code\":\"LO\",\"name\":\"Lodi\"},\"861\":{\"code\":\"LU\",\"name\":\"Lucca\"},\"862\":{\"code\":\"MC\",\"name\":\"Macerata\"},\"863\":{\"code\":\"MN\",\"name\":\"Mantova\"},\"864\":{\"code\":\"MS\",\"name\":\"Massa-Carrara\"},\"865\":{\"code\":\"MT\",\"name\":\"Matera\"},\"866\":{\"code\":\"VS\",\"name\":\"Medio Campidano\"},\"867\":{\"code\":\"ME\",\"name\":\"Messina\"},\"868\":{\"code\":\"MI\",\"name\":\"Milano\"},\"869\":{\"code\":\"MO\",\"name\":\"Modena\"},\"870\":{\"code\":\"MB\",\"name\":\"Monza-Brianza\"},\"871\":{\"code\":\"NA\",\"name\":\"Napoli\"},\"872\":{\"code\":\"NO\",\"name\":\"Novara\"},\"873\":{\"code\":\"NU\",\"name\":\"Nuoro\"},\"874\":{\"code\":\"OG\",\"name\":\"Ogliastra\"},\"875\":{\"code\":\"OT\",\"name\":\"Olbia Tempio\"},\"876\":{\"code\":\"OR\",\"name\":\"Oristano\"},\"877\":{\"code\":\"PD\",\"name\":\"Padova\"},\"878\":{\"code\":\"PA\",\"name\":\"Palermo\"},\"879\":{\"code\":\"PR\",\"name\":\"Parma\"},\"880\":{\"code\":\"PV\",\"name\":\"Pavia\"},\"881\":{\"code\":\"PG\",\"name\":\"Perugia\"},\"882\":{\"code\":\"PU\",\"name\":\"Pesaro-Urbino\"},\"883\":{\"code\":\"PE\",\"name\":\"Pescara\"},\"884\":{\"code\":\"PC\",\"name\":\"Piacenza\"},\"885\":{\"code\":\"PI\",\"name\":\"Pisa\"},\"886\":{\"code\":\"PT\",\"name\":\"Pistoia\"},\"887\":{\"code\":\"PN\",\"name\":\"Pordenone\"},\"888\":{\"code\":\"PZ\",\"name\":\"Potenza\"},\"889\":{\"code\":\"PO\",\"name\":\"Prato\"},\"890\":{\"code\":\"RG\",\"name\":\"Ragusa\"},\"891\":{\"code\":\"RA\",\"name\":\"Ravenna\"},\"892\":{\"code\":\"RC\",\"name\":\"Reggio-Calabria\"},\"893\":{\"code\":\"RE\",\"name\":\"Reggio-Emilia\"},\"894\":{\"code\":\"RI\",\"name\":\"Rieti\"},\"895\":{\"code\":\"RN\",\"name\":\"Rimini\"},\"896\":{\"code\":\"RM\",\"name\":\"Roma\"},\"897\":{\"code\":\"RO\",\"name\":\"Rovigo\"},\"898\":{\"code\":\"SA\",\"name\":\"Salerno\"},\"899\":{\"code\":\"SS\",\"name\":\"Sassari\"},\"900\":{\"code\":\"SV\",\"name\":\"Savona\"},\"901\":{\"code\":\"SI\",\"name\":\"Siena\"},\"902\":{\"code\":\"SR\",\"name\":\"Siracusa\"},\"903\":{\"code\":\"SO\",\"name\":\"Sondrio\"},\"904\":{\"code\":\"TA\",\"name\":\"Taranto\"},\"905\":{\"code\":\"TE\",\"name\":\"Teramo\"},\"906\":{\"code\":\"TR\",\"name\":\"Terni\"},\"907\":{\"code\":\"TO\",\"name\":\"Torino\"},\"908\":{\"code\":\"TP\",\"name\":\"Trapani\"},\"909\":{\"code\":\"TN\",\"name\":\"Trento\"},\"910\":{\"code\":\"TV\",\"name\":\"Treviso\"},\"911\":{\"code\":\"TS\",\"name\":\"Trieste\"},\"912\":{\"code\":\"UD\",\"name\":\"Udine\"},\"913\":{\"code\":\"VA\",\"name\":\"Varese\"},\"914\":{\"code\":\"VE\",\"name\":\"Venezia\"},\"915\":{\"code\":\"VB\",\"name\":\"Verbania\"},\"916\":{\"code\":\"VC\",\"name\":\"Vercelli\"},\"917\":{\"code\":\"VR\",\"name\":\"Verona\"},\"918\":{\"code\":\"VV\",\"name\":\"Vibo-Valentia\"},\"919\":{\"code\":\"VI\",\"name\":\"Vicenza\"},\"920\":{\"code\":\"VT\",\"name\":\"Viterbo\"}}},\"data_id\":1790602390},\"captcha\":[],\"instant-purchase\":[],\"loggedAsCustomer\":[],\"persistent\":[],\"review\":[],\"payments\":[],\"wishlist\":{\"items\":[]},\"amga4_event_pool\":[],\"amga4_hyva_event_pool\":[],\"recently_viewed_product\":[],\"recently_compared_product\":[],\"product_data_storage\":[],\"paypal-billing-agreement\":[]}\n\n function cookie() {\n return {\n ajaxAddCookieUrl: 'https:\/\/www.bici.it\/popup\/ajax\/addCookie\/',\n sendCountCookie: function () {\n\n fetch(\n this.ajaxAddCookieUrl,\n {\n method: 'get',\n data: JSON.stringify({\n isAjax: 'true',\n form_key: \"HUZADiXFWRyc9TTi\",\n cookie_type: 'aw_popup_viewed_page',\n current_url: 'https:\/\/www.bici.it\/catalog\/product\/view\/id\/85\/'\n }),\n headers: {contentType: 'application\/json'}\n }\n )\n },\n sendCookie: function (popupId, cookieType, cookieLifetime) {\n fetch(`${this.ajaxAddCookieUrl}'?isAjax=true\u0026amp;cookie_type=${cookieType}\u0026amp;popup_id=${popupId}\u0026amp;cookie_lifetime=${cookieLifetime}`,\n {\n method: 'GET',\n headers: {\n 'Content-Type': 'application\/json',\n 'X-Requested-With': 'XMLHttpRequest'\n }\n })\n }\n }\n }\n\n (()=\u0026gt; {\n 'use strict';\n const amBrandPosition = {\n selectors: {\n brands: '#maincontent \u0026gt; .container .amshopby-option-link',\n productInfoTitleP: '.product-info-main p.title-font'\n },\n\n amBrandPositionInit() {\n const brands = document.querySelectorAll(this.selectors.brands);\n let target;\n\n if (!brands.length) {\n return;\n }\n\n target = document.querySelector(this.selectors.productInfoTitleP);\n\n for (let brand of brands) {\n brand.classList.remove('flex');\n brand.classList.add('inline-flex');\n this.moveElement(brand, target);\n }\n },\n\n moveElement(element, target) {\n const fragment = document.createDocumentFragment();\n fragment.appendChild(element);\n target.append(fragment);\n element.classList.remove('hidden');\n }\n }\n document.addEventListener('DOMContentLoaded', () =\u0026gt; {\n amBrandPosition.amBrandPositionInit();\n })\n })()\n\n function popupManager() {\n return {\n cookiePregValue: 'aw_popup_viewed_popup_',\n _getCookie: function (cname) {\n var name = cname + \"=\",\n decodedCookie = decodeURIComponent(document.cookie),\n ca = decodedCookie.split(';'),\n i, c;\n\n for (i = 0; i \u0026lt; ca.length; i++) {\n c = ca[i];\n while (c.charAt(0) == ' ') {\n c = c.substring(1);\n }\n if (c.indexOf(name) == 0) {\n return c.substring(name.length, c.length);\n }\n }\n\n return '';\n },\n\n \/**\n * event for leave browser popup\n * see http:\/\/bradsknutson.com\/blog\/javascript-detect-mouse-leaving-browser-window\/\n *\/\n addEvent: function (obj, evt, fn) {\n if (obj.addEventListener) {\n obj.addEventListener(evt, fn, false);\n } else if (obj.attachEvent) {\n obj.attachEvent(\"on\" + evt, fn);\n }\n },\n\n popupSetClass: function (popup, value) {\n popup.setAttribute('class', value);\n }\n }\n }\n (() =\u0026gt; {\n const originalModal = window.hyva.modal;\n window.hyva.modal = function (options) {\n const instance = originalModal(options);\n const originalShow = instance.show;\n const originalHide = instance.hide;\n\n instance.show = function (value, event, key, cookieLifetime, equals, effect) {\n if (popupManager()._getCookie(popupManager().cookiePregValue + key) !== '') {\n return;\n }\n let popupElement = document.querySelector('.aheadworks_popup'+key);\n effect = effect.trim();\n\n let popupClasses = 'inline-block bg-white shadow-xl rounded-lg p-10 max-h-screen overflow-auto aw-'+effect+' aw-'+effect+'_in aheadworks_popup'+key\n\n switch (event) {\n case 'immediately_page_loaded':\n cookie().sendCookie(key, 'aw_popup_viewed_popup', cookieLifetime, true);\n originalShow.call(this, value);\n setTimeout(function () {\n window.dispatchEvent(new Event('resize'));\n popupManager().popupSetClass(popupElement, popupClasses);\n }, 100);\n break;\n case 'x_sec_after_duration':\n if (equals) {\n setTimeout(function () {\n cookie().sendCookie(key, 'aw_popup_viewed_popup', cookieLifetime, true);\n originalShow.call(this, value);\n setTimeout(function () {\n window.dispatchEvent(new Event('resize'));\n popupManager().popupSetClass(popupElement, popupClasses);\n }, 100);\n }.bind(this), equals * 1000);\n } else {\n originalShow.call(this, value)\n }\n break;\n case 'once_page_scrolled':\n window.onscroll = function (ev) {\n if (popupManager()._getCookie(popupManager().cookiePregValue + key) === '') {\n var offset = window.scrollY || document.documentElement.scrollTop,\n windowHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight,\n progress = Math.floor(offset \/ windowHeight * 100);\n\n if (progress \u0026gt;= equals) {\n cookie().sendCookie(key, 'aw_popup_viewed_popup', cookieLifetime, true);\n originalShow.call(this, value);\n setTimeout(function () {\n window.dispatchEvent(new Event('resize'));\n popupManager().popupSetClass(popupElement, popupClasses);\n }, 100);\n }\n }\n }.bind(this);\n break;\n case 'once_customer_pages_viewed':\n cookie().sendCookie(key, 'aw_popup_viewed_popup', cookieLifetime, true);\n originalShow.call(this, value);\n setTimeout(function () {\n window.dispatchEvent(new Event('resize'));\n popupManager().popupSetClass(popupElement, popupClasses);\n }, 100);\n break;\n case 'once_cursor_leave_browser':\n popupManager().addEvent(document, \"mouseout\", function (e) {\n e = e ? e : window.event;\n var from = e.relatedTarget || e.toElement;\n if (!from || from.nodeName === \"HTML\") {\n if (popupManager()._getCookie(popupManager().cookiePregValue + key) === '') {\n cookie().sendCookie(key, 'aw_popup_viewed_popup', cookieLifetime, true);\n originalShow.call(this, value);\n setTimeout(function () {\n window.dispatchEvent(new Event('resize'));\n popupManager().popupSetClass(popupElement, popupClasses);\n }, 100);\n }\n }\n }.bind(this));\n break;\n }\n\n }\n\n instance.hide = function (value, effect, key) {\n originalHide.call(this, value);\n let popupElement = document.querySelector('.aheadworks_popup'+key),\n popupClasses = 'inline-block bg-white shadow-xl rounded-lg p-10 max-h-screen overflow-auto aw-' + effect + ' aw-' + effect + '_out aheadworks_popup' + key;\n\n popupManager().popupSetClass(popupElement, popupClasses);\n }\n return instance;\n }\n window.hyva.modal.eventListeners = originalModal.eventListeners;\n window.hyva.modal.excludeSelectorsFromFocusTrap = originalModal.excludeSelectorsFromFocusTrap;\n window.hyva.modal.pop = originalModal.pop;\n })(),\n\n (function () {\n const originalListener = window.hyva.modal.eventListeners.click;\n document.removeEventListener('click', originalListener);\n window.hyva.modal.eventListeners.keydown = event =\u0026gt; {\n var path = event.path || (event.composedPath \u0026amp;\u0026amp; event.composedPath());\n if (path) {\n path.forEach(function (element, k) {\n var key;\n if (!this.event.target.classList.contains('aw-close-popup')) {\n if (path[k].id) {\n if (path[k].id.includes('aheadworks_popup')) {\n key = path[k].id.replace(\/[^0-9]\/g, \"\");\n let sendStatistic = false;\n path.forEach(function (elemin, keyin) {\n if(elemin.tagName) {\n if (elemin.tagName === 'A' || elemin.tagName === 'BUTTON') {\n sendStatistic = true;\n return;\n }\n }\n })\n\n if(sendStatistic) {\n cookie().sendCookie(key, 'aw_popup_used_popup', 0, true);\n }\n }\n }\n }\n })\n }\n originalListener(event);\n };\n\n document.addEventListener('click', window.hyva.modal.eventListeners.keydown);\n })()\u003c\/any\u003e\u003c\/p\u003e","brand":"Fratelli Sala Srl","offers":[{"title":"Default Title","offer_id":55193960513863,"sku":"3fbca908-84be-465e-9539-db0eabc390d8","price":579.7,"currency_code":"EUR","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0844\/0348\/7047\/files\/0116834300_899_dfa6a072-7929-40ea-bc68-a84d8ce8716f.jpg?v=1790604010","url":"https:\/\/www.weply.it\/en-eu\/products\/whistle-miwok-2160-27-5-2026-km0-2","provider":"Weply","version":"1.0","type":"link"}