1const dialog = document.querySelector('[data-inquiry-dialog]'); 2 3if (dialog instanceof HTMLDialogElement) { 4 const form = dialog.querySelector('[data-inquiry-form]'); 5 const header = dialog.querySelector('.landing-inquiry__header'); 6 const success = dialog.querySelector('[data-inquiry-success]'); 7 const error = dialog.querySelector('[data-inquiry-error]'); 8 const submits = [...dialog.querySelectorAll('[data-inquiry-submit]')]; 9 const submit = submits[0]; 10 const requestedContext = dialog.querySelector('[data-inquiry-requested-context]'); 11 const program = dialog.querySelector('[name="soma_program"], [name="story_room_program"]'); 12 const somaRole = dialog.querySelector('[name="soma_role"]'); 13 const somaRequestType = dialog.querySelector('[name="soma_request_type"]'); 14 const somaVersion = dialog.querySelector('[name="soma_version"]'); 15 const inquiryDetails = dialog.querySelector('[data-inquiry-details]'); 16 const pageContext = requestedContext?.value || ''; 17 const selection = dialog.querySelector('[data-inquiry-selection]'); 18 const duration = dialog.querySelector('[data-inquiry-duration]'); 19 const price = dialog.querySelector('[data-inquiry-price]'); 20 const dailyPrice = dialog.querySelector('[data-inquiry-daily-price]'); 21 const startDate = dialog.querySelector('[data-inquiry-start-date]'); 22 const endDate = dialog.querySelector('[data-inquiry-end-date]'); 23 const optionPrices = dialog.querySelector('[data-inquiry-option-prices]'); 24 const panel = dialog.querySelector('.landing-inquiry__panel'); 25 const initialPrice = price?.textContent || ''; 26 const inquiryHistoryKey = 'landingInquiryOpen'; 27 const mobileInquiry = window.matchMedia('(max-width: 42rem)'); 28 const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); 29 const attributionKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'partner_ref']; 30 let opener = null; 31 let swipe = null; 32 let swipeTimer = null; 33 let inquiryStage = 'closed'; 34 let inquiryCompleted = false; 35 let inquiryCloseTracked = true; 36 let inquiryDetailsTracked = false; 37 let inquiryEntryPoint = 'direct'; 38 39 const quotePath = /\/(?:preventivo|richiedi-preventivo|quote|devis|angebot)\/?$/i; 40 const optionPrice = (select) => select?.selectedOptions?.[0]?.dataset.price?.trim() || ''; 41 const selectedOption = (select) => select?.selectedOptions?.[0]; 42 const pageUrl = () => `${window.location.pathname}${window.location.search}`; 43 const inquiryUrl = () => `${pageUrl()}#richiesta`; 44 const inquiryTarget = () => form?.dataset.inquiryOfferingKey || form?.dataset.inquiryKey || 'general_inquiry'; 45 const inquiryContext = (action = '') => ({ 46 offering_kind: form?.dataset.inquiryOfferingKind || 'project', 47 offering_key: inquiryTarget(), 48 market: form?.dataset.inquiryMarket || '', 49 locale: form?.dataset.inquiryLocale || '', 50 entry_point: inquiryEntryPoint, 51 request_type: somaRequestType?.value || '', 52 product_variant: somaVersion?.value === 'Dome 6' ? 'dome_6' : (somaVersion?.value === 'Room' ? 'room' : ''), 53 commercial_program: program?.value || '', 54 action, 55 }); 56 const trackInquiry = (eventName, action = '') => { 57 const context = inquiryContext(action); 58 if (typeof window.tunCommerceTrack === 'function') { 59 window.tunCommerceTrack(eventName, inquiryTarget(), context); 60 return; 61 } 62 window.tunAnalyticsTrack?.(eventName, { 63 interaction_target: inquiryTarget(), 64 ...context, 65 }); 66 }; 67 const trackInquiryClose = () => { 68 if (inquiryCloseTracked || inquiryCompleted || inquiryStage === 'closed') return; 69 inquiryCloseTracked = true; 70 trackInquiry('inquiry_closed', inquiryStage); 71 }; 72 const updatePeriodConstraints = () => { 73 if (!startDate || !endDate) return; 74 if (startDate.value) endDate.min = startDate.value; 75 else endDate.removeAttribute('min'); 76 }; 77 const markInquiryStarted = () => { 78 if (inquiryStage !== 'opened') return; 79 inquiryStage = 'started'; 80 trackInquiry('inquiry_started', 'form_interaction'); 81 }; 82 83 const resetSwipe = () => { 84 window.clearTimeout(swipeTimer); 85 swipeTimer = null; 86 swipe = null; 87 dialog.removeAttribute('data-inquiry-dragging'); 88 dialog.removeAttribute('data-inquiry-settling'); 89 panel?.style.removeProperty('--inquiry-swipe-x'); 90 }; 91 92 const settleSwipe = (offset, callback) => { 93 if (!panel) { 94 callback?.(); 95 return; 96 } 97 98 dialog.removeAttribute('data-inquiry-dragging'); 99 dialog.setAttribute('data-inquiry-settling', ''); 100 panel.style.setProperty('--inquiry-swipe-x', `${offset}px`); 101 swipeTimer = window.setTimeout(() => { 102 if (callback) { 103 swipeTimer = null; 104 callback(); 105 return; 106 } 107 resetSwipe(); 108 }, reducedMotion.matches ? 0 : 230); 109 }; 110 111 const close = () => { 112 if (!dialog.open) return; 113 if (window.history.state?.[inquiryHistoryKey]) { 114 window.history.back(); 115 return; 116 } 117 dialog.close(); 118 }; 119 120 const updatePrice = () => { 121 const selectedPrice = optionPrice(selection) || optionPrice(duration) || initialPrice; 122 if (dailyPrice) dailyPrice.textContent = selectedPrice; 123 124 if (price && form?.dataset.inquiryPricing === 'daily-duration') { 125 const item = selectedOption(selection);
126 const unitAmount = Number(item?.dataset.unitAmount || ''); 127 const currency = item?.dataset.currency || ''; 128 const start = Date.parse(`${startDate?.value || ''}T00:00:00Z`); 129 const end = Date.parse(`${endDate?.value || ''}T00:00:00Z`); 130 const days = Number.isFinite(start) && Number.isFinite(end) && end >= start 131 ? Math.round((end - start) / 86400000) + 1 132 : 0; 133 134 if (days > 0 && Number.isFinite(unitAmount) && unitAmount > 0 && currency) { 135 const amount = new Intl.NumberFormat(form.dataset.inquiryLocale || undefined, { 136 maximumFractionDigits: 2, 137 }).format(unitAmount * days); 138 const dayLabel = days === 1 ? form.dataset.inquiryDaySingular : form.dataset.inquiryDayPlural; 139 price.textContent = `${currency} ${amount} · ${days} ${dayLabel}`; 140 } else { 141 price.textContent = form.dataset.inquiryTotalPlaceholder || selectedPrice; 142 } 143 } else if (price) { 144 price.textContent = selectedPrice; 145 } 146 if (!optionPrices) return; 147 148 const selected = [...dialog.querySelectorAll('[data-inquiry-option]:checked')] 149 .map((input) => ({ label: input.closest('label')?.querySelector('span')?.textContent?.trim(), price: input.dataset.price?.trim() })) 150 .filter((item) => item.price); 151 152 optionPrices.replaceChildren(...selected.map((item) => { 153 const row = document.createElement('li'); 154 row.textContent = `${item.label}: ${item.price}`; 155 return row; 156 })); 157 optionPrices.hidden = selected.length === 0; 158 }; 159 160 const selectionFromUrl = (href) => { 161 try { 162 const url = new URL(href, window.location.href); 163 return { 164 key: url.searchParams.get('bundle') || url.searchParams.get('product') || url.searchParams.get('service') || '', 165 context: [...url.searchParams.entries()].map(([key, value]) => `${key}: ${value}`).join(' · '), 166 }; 167 } catch { 168 return { key: '', context: '' }; 169 } 170 }; 171 172 document.querySelectorAll('a[href]').forEach((link) => { 173 const source = link.getAttribute('href') || ''; 174 let url; 175 try { url = new URL(source, window.location.href); } catch { return; } 176 177 if (url.origin === window.location.origin && quotePath.test(url.pathname)) { 178 const parsed = selectionFromUrl(source); 179 link.dataset.inquiryOpen = ''; 180 link.dataset.inquirySelection ||= parsed.key; 181 link.dataset.inquiryContext ||= parsed.context; 182 link.setAttribute('href', '#richiesta'); 183 } 184 }); 185 186 const open = (trigger, explicitContext = '') => { 187 const wasOpen = dialog.open; 188 opener = trigger instanceof HTMLElement ? trigger : null; 189 inquiryEntryPoint = trigger?.dataset.inquirySource || trigger?.dataset.analyticsAction || 'direct'; 190 const key = trigger?.dataset.inquirySelection || ''; 191 const optionKey = trigger?.dataset.inquiryOptionKey || ''; 192 const context = explicitContext || trigger?.dataset.inquiryContext || trigger?.textContent?.trim() || ''; 193 const matchingSelect = [selection, duration].find((select) => select && [...select.options].some((option) => option.value === key)); 194 if (matchingSelect && key) matchingSelect.value = key; 195 if (trigger?.hasAttribute('data-inquiry-clear-selection')) { 196 if (selection) selection.value = ''; 197 if (duration) duration.value = ''; 198 } 199 if (optionKey) { 200 const option = dialog.querySelector(`[data-inquiry-option][value="${CSS.escape(optionKey)}"]`); 201 if (option) option.checked = true; 202 } 203 if (requestedContext) requestedContext.value = [pageContext, context].filter(Boolean).join(' · '); 204 if (program) { 205 program.value = trigger?.dataset.inquiryProgram || ''; 206 } 207 const inquirySomaRole = trigger?.dataset.inquirySomaRole || ''; 208 if (somaRole && inquirySomaRole && [...somaRole.options].some((option) => option.value === inquirySomaRole)) { 209 somaRole.value = inquirySomaRole; 210 } 211 if (somaRequestType) { 212 somaRequestType.value = trigger?.dataset.inquirySomaRequestType || 'assessment'; 213 } 214 const inquirySomaVersion = trigger?.dataset.inquirySomaVersion || ''; 215 if (somaVersion && inquirySomaVersion && [...somaVersion.options].some((option) => option.value === inquirySomaVersion)) { 216 somaVersion.value = inquirySomaVersion; 217 } 218 const inquirySomaPartnerModel = trigger?.dataset.inquirySomaPartnerModel || ''; 219 const somaPartnerModel = dialog.querySelector('[name="soma_partner_model"]'); 220 if (somaPartnerModel && inquirySomaPartnerModel && [...somaPartnerModel.options].some((option) => option.value === inquirySomaPartnerModel)) { 221 somaPartnerModel.value = inquirySomaPartnerModel; 222 } 223 try { 224 const scenario = JSON.parse(trigger?.dataset.inquirySomaScenario || '{}');
225 Object.entries(scenario).forEach(([name, value]) => { 226 if (!name.startsWith('soma_')) return; 227 const field = dialog.querySelector(`[name="${CSS.escape(name)}"]`); 228 if (field instanceof HTMLInputElement || field instanceof HTMLSelectElement) field.value = String(value); 229 }); 230 } catch { 231 // An invalid optional prefill must never prevent the inquiry from opening. 232 } 233 if (error) { error.hidden = true; error.textContent = ''; } 234 updatePeriodConstraints(); 235 updatePrice(); 236 if (!dialog.open) { 237 window.history.pushState( 238 { ...window.history.state, [inquiryHistoryKey]: true }, 239 '', 240 inquiryUrl(), 241 ); 242 dialog.showModal(); 243 } 244 if (!wasOpen) { 245 inquiryStage = 'opened'; 246 inquiryCompleted = false; 247 inquiryCloseTracked = false; 248 inquiryDetailsTracked = false; 249 trackInquiry('inquiry_opened', 'open'); 250 window.tunCommercialAnalytics?.trackFormOpen(form, 'landing_inquiry'); 251 } 252 document.dispatchEvent(new CustomEvent('tun:turnstile-needed')); 253 }; 254 255 selection?.addEventListener('change', updatePrice); 256 duration?.addEventListener('change', updatePrice); 257 startDate?.addEventListener('change', () => { 258 updatePeriodConstraints(); 259 updatePrice(); 260 }); 261 endDate?.addEventListener('change', updatePrice); 262 dialog.querySelectorAll('[data-inquiry-option]').forEach((input) => input.addEventListener('change', updatePrice)); 263 inquiryDetails?.addEventListener('toggle', () => { 264 if (!inquiryDetails.open || inquiryDetailsTracked || inquiryStage === 'closed') return; 265 inquiryDetailsTracked = true; 266 trackInquiry('inquiry_details_opened', 'optional_details'); 267 }); 268 dialog.querySelector('[data-inquiry-options-toggle]')?.addEventListener('click', (event) => { 269 dialog.querySelectorAll('[data-inquiry-option-extra]').forEach((option) => { option.hidden = false; }); 270 event.currentTarget.setAttribute('aria-expanded', 'true'); 271 event.currentTarget.hidden = true; 272 }); 273 274 document.addEventListener('click', (event) => { 275 const trigger = event.target.closest('[data-inquiry-open], [data-ep-form-open], a[href="#richiesta"]'); 276 if (!trigger) return; 277 event.preventDefault(); 278 open(trigger); 279 }); 280 281 document.querySelectorAll('[data-inquiry-source-form]').forEach((sourceForm) => { 282 sourceForm.addEventListener('submit', (event) => { 283 event.preventDefault(); 284 if (!sourceForm.reportValidity()) return; 285 286 const details = [...new FormData(sourceForm).entries()] 287 .filter(([name, value]) => name !== '_token' && String(value).trim() !== '') 288 .map(([name, value]) => `${name}: ${String(value).trim()}`) 289 .join(' · '); 290 open(sourceForm, [sourceForm.dataset.inquiryContext, details].filter(Boolean).join(' · ')); 291 }); 292 }); 293 294 if (window.location.hash === '#richiesta') { 295 window.history.replaceState( 296 { ...window.history.state, [inquiryHistoryKey]: false }, 297 '', 298 pageUrl(), 299 ); 300 open(null); 301 } 302 303 dialog.querySelectorAll('[data-inquiry-close]').forEach((button) => button.addEventListener('click', close)); 304 dialog.addEventListener('keydown', (event) => { 305 if (!['corporate-brand-experience', 'partner-project'].includes(dialog.dataset.inquiryVariant) || event.key !== 'Tab' || !dialog.open) return; 306 307 const focusable = [...dialog.querySelectorAll('a[href], button, input, select, textarea, summary, [tabindex]')] 308 .filter((element) => !element.hasAttribute('disabled') 309 && element.getAttribute('tabindex') !== '-1' 310 && !(element.closest('details:not([open])') && element.tagName !== 'SUMMARY') 311 && element.getClientRects().length > 0); 312 if (!focusable.length) return; 313 314 const first = focusable[0];
315 const last = focusable[focusable.length - 1]; 316 if (event.shiftKey && (document.activeElement === first || !dialog.contains(document.activeElement))) { 317 event.preventDefault(); 318 last.focus(); 319 } else if (!event.shiftKey && document.activeElement === last) { 320 event.preventDefault(); 321 first.focus(); 322 } 323 }); 324 dialog.addEventListener('cancel', (event) => { 325 event.preventDefault(); 326 close(); 327 }); 328 window.addEventListener('popstate', (event) => { 329 if (event.state?.[inquiryHistoryKey]) { 330 if (!dialog.open) dialog.showModal(); 331 return; 332 } 333 if (dialog.open) dialog.close(); 334 }); 335 dialog.addEventListener('close', () => { 336 trackInquiryClose(); 337 inquiryStage = 'closed'; 338 resetSwipe(); 339 opener?.focus(); 340 }); 341 window.addEventListener('pagehide', trackInquiryClose, { once: true }); 342 343 form?.addEventListener('input', markInquiryStarted); 344 form?.addEventListener('change', markInquiryStarted); 345 form?.addEventListener('invalid', (event) => { 346 if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement || event.target instanceof HTMLTextAreaElement) { 347 event.target.setAttribute('aria-invalid', 'true'); 348 } 349 }, true); 350 form?.addEventListener('input', (event) => { 351 if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement || event.target instanceof HTMLTextAreaElement) { 352 if (event.target.checkValidity()) event.target.removeAttribute('aria-invalid'); 353 } 354 }); 355 356 panel?.addEventListener('touchstart', (event) => { 357 if (!mobileInquiry.matches || event.touches.length !== 1) return; 358 const touch = event.touches[0]; 359 swipe = { 360 startX: touch.clientX, 361 startY: touch.clientY, 362 offset: 0, 363 startedAt: window.performance.now(), 364 dragging: false, 365 }; 366 }, { passive: true }); 367 368 panel?.addEventListener('touchmove', (event) => { 369 if (!swipe || event.touches.length !== 1) return; 370 const touch = event.touches[0]; 371 const deltaX = touch.clientX - swipe.startX; 372 const deltaY = touch.clientY - swipe.startY; 373 374 if (!swipe.dragging) { 375 if (Math.abs(deltaY) > 10 && Math.abs(deltaY) > deltaX) { 376 swipe = null; 377 return; 378 } 379 if (deltaX <= 10 || deltaX <= Math.abs(deltaY) * 1.15) return; 380 swipe.dragging = true; 381 dialog.setAttribute('data-inquiry-dragging', ''); 382 } 383 384 event.preventDefault(); 385 swipe.offset = Math.max(0, deltaX); 386 panel.style.setProperty('--inquiry-swipe-x', `${swipe.offset}px`); 387 }, { passive: false }); 388 389 const finishSwipe = () => { 390 if (!swipe?.dragging || !panel) { 391 swipe = null; 392 return; 393 } 394 395 const elapsed = Math.max(1, window.performance.now() - swipe.startedAt); 396 const velocity = swipe.offset / elapsed; 397 const shouldClose = swipe.offset >= Math.max(72, panel.clientWidth * .24) 398 || (swipe.offset >= 48 && velocity > .5); 399 400 if (shouldClose) { 401 settleSwipe(window.innerWidth, close); 402 return; 403 } 404 settleSwipe(0); 405 }; 406 407 panel?.addEventListener('touchend', finishSwipe, { passive: true }); 408 panel?.addEventListener('touchcancel', () => { 409 if (swipe?.dragging) settleSwipe(0); 410 else swipe = null; 411 }, { passive: true }); 412 413 form?.addEventListener('submit', async (event) => { 414 event.preventDefault(); 415 markInquiryStarted(); 416 inquiryStage = 'submit_attempted'; 417 trackInquiry('inquiry_submit_attempted', 'submit'); 418 if (!form.reportValidity()) { 419 inquiryStage = 'validation_failed'; 420 trackInquiry('inquiry_validation_failed', 'client_validation'); 421 window.tunCommercialAnalytics?.trackFormError(form, 'landing_inquiry'); 422 return; 423 } 424 if (!submit) return; 425 const originalLabels = submits.map((button) => button.textContent); 426 form.setAttribute('aria-busy', 'true');
427 submits.forEach((button) => { 428 button.disabled = true; 429 button.textContent = form.dataset.sendingLabel; 430 }); 431 if (error) { error.hidden = true; error.textContent = ''; } 432 433 try { 434 await window.tunAnalyticsAttribution?.(); 435 const formData = new FormData(form); 436 const currentQuery = new URLSearchParams(window.location.search); 437 attributionKeys.forEach((key) => { 438 const value = currentQuery.get(key)?.trim(); 439 if (value && !formData.has(key)) formData.append(key, value); 440 }); 441 const response = await fetch(form.action, { 442 method: 'POST', 443 body: formData, 444 headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, 445 }); 446 const data = await response.json().catch(() => ({})); 447 if (!response.ok) { 448 throw new Error(form.dataset.errorMessage); 449 } 450 451 if (header) header.hidden = true; 452 form.hidden = true; 453 success.hidden = false; 454 success.textContent = data.message; 455 success.focus(); 456 inquiryCompleted = true; 457 inquiryCloseTracked = true; 458 inquiryStage = 'confirmed'; 459 const offeringKey = form.dataset.inquiryOfferingKey || form.dataset.inquiryKey || 'general_inquiry'; 460 const offeringKind = form.dataset.inquiryOfferingKind || 'project'; 461 const requestType = somaRequestType?.value || ''; 462 const conversionType = { 463 business_case: 'business_case_request', 464 demo: 'demo_request', 465 partner_project: 'partner_project', 466 lighthouse_candidate: 'lighthouse_candidate', 467 }[requestType] || (dialog.dataset.inquiryVariant === 'availability' ? 'availability_request' : 'landing_inquiry'); 468 window.tunAnalyticsTrack?.('lead_confirmation_viewed', { 469 transaction_id: data.transaction_id || '', 470 conversion_type: conversionType, 471 landing_key: form.dataset.inquiryKey || 'general_inquiry', 472 offering_kind: offeringKind, 473 offering_key: offeringKey, 474 market: form.dataset.inquiryMarket || '', 475 locale: form.dataset.inquiryLocale || '', 476 entry_point: inquiryEntryPoint, 477 request_type: requestType, 478 product_variant: somaVersion?.value === 'Dome 6' ? 'dome_6' : (somaVersion?.value === 'Room' ? 'room' : ''), 479 commercial_program: program?.value || '', 480 source_system: 'website_form', 481 }); 482 window.tunCommercialAnalytics?.trackFormSuccess( 483 form, 484 `landing-inquiry:${data.transaction_id || data.reference || ''}`, 485 'landing_inquiry', 486 ); 487 form.dispatchEvent(new CustomEvent('tun:inquiry-confirmed', { 488 bubbles: true, 489 detail: { 490 reference: data.reference || '', 491 transaction_id: data.transaction_id || '', 492 offering_key: offeringKey, 493 }, 494 })); 495 } catch (failure) { 496 inquiryStage = 'submit_failed'; 497 trackInquiry('inquiry_submit_failed', 'request_failed'); 498 window.tunCommercialAnalytics?.trackFormError(form, 'landing_inquiry'); 499 error.textContent = failure.message || form.dataset.errorMessage; 500 error.hidden = false; 501 error.focus?.(); 502 } finally { 503 form.setAttribute('aria-busy', 'false'); 504 submits.forEach((button, index) => { 505 button.disabled = false; 506 button.textContent = originalLabels[index]; 507 }); 508 } 509 }); 510 511}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.