1'use strict'; 2document.addEventListener('DOMContentLoaded', () => { 3 const configNode=document.getElementById('netz-theme-config'); 4 if(!configNode)throw new Error('Missing shared theme configuration.'); 5 const theme=JSON.parse(configNode.textContent); 6 if(!theme.language || !theme.strings || !theme.booking)throw new Error('Incomplete theme configuration.'); 7 const plural=new Intl.PluralRules(theme.language); 8 const t=(key,values={},count)=>{ 9 const entry=theme.strings[key]; 10 let text=count===undefined?entry:entry?.[plural.select(count)]; 11 if(typeof text!=='string')throw new Error('Missing theme translation: '+key); 12 for(const [name,value] of Object.entries(values))text=text.replaceAll('{'+name+'}',String(value)); 13 if(/\{[a-zA-Z]+\}/.test(text))throw new Error('Missing translation value: '+key); 14 return text; 15 }; 16 const desktopContents=window.matchMedia('(min-width: 1000px)'); 17 document.querySelectorAll('.pz-ux-with-contents .pz-ux-contents').forEach(details=>{ 18 const summary=details.querySelector('summary'); 19 if(!summary)throw new Error('Contents panel requires its summary.'); 20 const syncContents=()=>{details.open=desktopContents.matches;summary.tabIndex=desktopContents.matches?-1:0;}; 21 syncContents();desktopContents.addEventListener('change',syncContents); 22 summary.addEventListener('click',event=>{if(desktopContents.matches)event.preventDefault();}); 23 }); 24 const measure=(action,pageId,placement)=>{ 25 const detail={action,pageId,placement,pageType:document.querySelector('main')?.className,device:desktopContents.matches?'desktop':'mobile'}; 26 document.dispatchEvent(new CustomEvent('poezdka:journey-event',{detail})); 27 if(window._paq && typeof window._paq.push==='function')window._paq.push(['trackEvent','Poezdka journey',action,String(pageId)+'|'+placement+'|'+detail.device]); 28 else document.documentElement.dataset.pzAnalytics='tracker-not-configured'; 29 }; 30 document.addEventListener('poezdka:booking-search',event=>measure('booking-submit',event.detail.pageId,event.detail.placement)); 31 document.querySelectorAll('[data-pz-booking-link]').forEach(a=>a.addEventListener('click',()=>measure('booking-link',a.dataset.pzBookingLink,'gallery-end'))); 32 document.querySelectorAll('main a[href]').forEach(a=>{ 33 const destination=new URL(a.href); 34 if(destination.origin!==location.origin || !/^\/(113|901)\//.test(destination.pathname))return; 35 a.addEventListener('click',()=>measure('destination-select',location.pathname,a.closest('.breadcrumb')?'breadcrumb':a.closest('.pz-ux-directory,.pz-ux-lower-destinations')?'directory':'article')); 36 }); 37 const bookingPanel=document.getElementById('pz-booking'); 38 if(bookingPanel){const observer=new IntersectionObserver(entries=>{if(entries.some(e=>e.isIntersecting)){measure('booking-view',bookingPanel.querySelector('form').dataset.pzPageId,'primary');observer.disconnect();}},{threshold:.25});observer.observe(bookingPanel);} 39 const form = document.getElementById('pzBookingForm'); 40 if (form) { 41 const required = (id) => { const el=document.getElementById(id); if(!el) throw new Error('Booking: fehlendes Feld '+id); return el; }; 42 const error=required('pz-booking-error'); 43 const fail=(message)=>{error.textContent=message;error.hidden=false;}; 44 const destination=required('pz-destination'),checkin=required('pz-checkin'),checkout=required('pz-checkout'); 45 // Destination suggestions use the existing configured place-search service. 46 const placeList=document.createElement('div'),placeStatus=document.createElement('div'); 47 placeList.id='pz-destination-options';placeList.className='pz-destination-options';placeList.hidden=true;placeList.setAttribute('role','listbox');placeList.setAttribute('aria-label',t('destinations')); 48 placeStatus.className='pz-destination-status';placeStatus.setAttribute('role','status');placeStatus.setAttribute('aria-live','polite'); 49 destination.parentNode.append(placeList,placeStatus); 50 destination.setAttribute('role','combobox');destination.setAttribute('aria-autocomplete','list');destination.setAttribute('aria-controls',placeList.id);destination.setAttribute('aria-expanded','false'); 51 let placeTimer,placeRequest,placeVersion=0,placeItems=[],placeActive=-1; 52 const closePlaces=()=>{placeList.hidden=true;destination.setAttribute('aria-expanded','false');destination.removeAttribute('aria-activedescendant');placeActive=-1;}; 53 const cancelPlaces=()=>{clearTimeout(placeTimer);placeVersion++;if(placeRequest)placeRequest.abort();closePlaces();}; 54 const selectPlace=index=>{destination.value=placeItems[index].value;cancelPlaces();placeStatus.textContent='';destination.focus();}; 55 const highlightPlace=index=>{placeActive=index;
55[...placeList.children].forEach((el,i)=>el.setAttribute('aria-selected',String(i===index)));destination.setAttribute('aria-activedescendant',placeList.children[index].id);placeList.children[index].scrollIntoView({block:'nearest'});}; 56 destination.addEventListener('input',()=>{ 57 cancelPlaces();placeList.replaceChildren();placeItems=[];placeStatus.textContent=''; 58 const query=destination.value.trim(),version=placeVersion;if(query.length<2)return; 59 placeTimer=setTimeout(async()=>{ 60 placeRequest=new AbortController();placeStatus.textContent=t('suggestionsLoading'); 61 try{ 62 const response=await fetch(theme.booking.suggestionsEndpoint+'?q='+encodeURIComponent(query),{signal:placeRequest.signal,credentials:'same-origin'}); 63 if(!response.ok)throw new Error('Place search failed');const result=await response.json(); 64 if(version!==placeVersion)return; 65 if(!Array.isArray(result.data)||result.data.some(p=>!p||typeof p.label!=='string'||typeof p.value!=='string'||!p.label.trim()||!p.value.trim()))throw new Error('Invalid place response'); 66 placeItems=result.data;placeStatus.textContent=placeItems.length?'':t('suggestionsEmpty'); 67 placeItems.forEach((place,index)=>{const option=document.createElement('div');option.id='pz-place-'+index;option.setAttribute('role','option');option.setAttribute('aria-selected','false');option.textContent=place.label;option.addEventListener('pointerdown',event=>event.preventDefault());option.addEventListener('click',()=>selectPlace(index));placeList.append(option);}); 68 placeList.hidden=!placeItems.length;destination.setAttribute('aria-expanded',String(!!placeItems.length)); 69 }catch(e){if(e.name==='AbortError'||version!==placeVersion)return;closePlaces();placeStatus.textContent=t('suggestionsError');} 70 },180); 71 }); 72 destination.addEventListener('keydown',event=>{ 73 if(event.key==='Escape'){cancelPlaces();placeStatus.textContent='';return;} 74 if(placeList.hidden)return; 75 if(event.key==='ArrowDown'||event.key==='ArrowUp'){event.preventDefault();highlightPlace(placeActive<0?(event.key==='ArrowDown'?0:placeItems.length-1):(placeActive+(event.key==='ArrowDown'?1:placeItems.length-1))%placeItems.length);} 76 else if(event.key==='Enter'&&placeActive>=0){event.preventDefault();selectPlace(placeActive);} 77 else if(event.key==='Tab')cancelPlaces(); 78 }); 79 destination.addEventListener('blur',()=>{cancelPlaces();placeStatus.textContent='';}); 80 const adults=required('pz-adults'),children=required('pz-children'),rooms=required('pz-rooms'); 81 const ages=required('pz-child-ages'),summary=required('pz-guest-summary'); 82 const controls=[...document.querySelectorAll('button[form="pzBookingForm"],#pzBookingForm button[type="submit"]')]; 83 const identity=form.querySelector('[name="label"]'),aid=form.querySelector('[name="aid"]'); 84 if(!identity || !/^[a-z0-9]+$/.test(theme.booking.labelPrefix) || !identity.value.startsWith(theme.booking.labelPrefix) || !/^[1-9][0-9]*(?:x[1-9][0-9]*)?$/.test(identity.value.slice(theme.booking.labelPrefix.length)) || !aid || aid.value!==theme.booking.affiliateId || form.action!==theme.booking.action) { 85 controls.forEach(b=>b.disabled=true);fail(t('configurationError'));return; 86 } 87 const today=new Date();const localDate=[today.getFullYear(),String(today.getMonth()+1).padStart(2,'0'),String(today.getDate()).padStart(2,'0')].join('-'); 88 checkin.min=localDate;checkout.min=localDate; 89 function guests(){ 90 const count=Number(children.value); 91 while(ages.children.length>count)ages.lastElementChild.remove(); 92 while(ages.children.length<count){ 93 const n=ages.children.length+1,wrap=document.createElement('div'),label=document.createElement('label'),select=document.createElement('select'); 94 wrap.className='pz-ux-field';select.id='pz-age-'+n;select.name='age';select.required=true;label.htmlFor=select.id;label.textContent=t('childAge',{number:n}); 95 const empty=document.createElement('option');empty.value='';empty.textContent=t('select');select.append(empty); 96 for(let age=0;age<=17;
96age++){const o=document.createElement('option');o.value=String(age);o.textContent=age===0?t('underOne'):t('years',{count:age},age);select.append(o);} 97 wrap.append(label,select);ages.append(wrap); 98 } 99 summary.textContent=[['adults',Number(adults.value)],['children',count],['rooms',Number(rooms.value)]].map(([key,n])=>t(key,{count:n},n)).join(' · '); 100 } 101 [adults,children,rooms].forEach(el=>el.addEventListener('change',guests));guests(); 102 form.addEventListener('invalid',event=>{for(let parent=event.target.parentElement;parent && parent!==form;parent=parent.parentElement)if(parent.tagName==='DETAILS')parent.open=true;},true); 103 form.addEventListener('submit',event=>{ 104 event.preventDefault(); 105 error.hidden=true; 106 if(!destination.value.trim()){event.preventDefault();fail(t('destinationRequired'));destination.focus();return;} 107 if(Boolean(checkin.value)!==Boolean(checkout.value) || (checkin.value && (checkin.value<localDate || checkout.value<=checkin.value))){ 108 event.preventDefault();form.querySelectorAll('details').forEach(d=>d.open=true);fail(t('datesInvalid'));checkin.focus();return; 109 } 110 if(ages.querySelectorAll('select').length!==Number(children.value) || [...ages.querySelectorAll('select')].some(s=>s.value==='')){ 111 event.preventDefault();form.querySelectorAll('details').forEach(d=>d.open=true);fail(t('agesRequired'));return; 112 } 113 // Empty dates are an explicit supported search mode, not substituted dates. 114 const url=new URL(theme.booking.action); 115 if(form.method.toLowerCase()!=='get' || url.protocol!=='https:')throw new Error('Invalid Booking search contract.'); 116 const params=new URLSearchParams(new FormData(form)); 117 if(!checkin.value)params.delete(checkin.name); 118 if(!checkout.value)params.delete(checkout.name); 119 for(const key of new Set(params.keys()))url.searchParams.delete(key); 120 for(const [key,value] of params)url.searchParams.append(key,value); 121 const next=window.open('about:blank','_blank'); 122 if(!next){fail(t('popupBlocked'));return;} 123 next.opener=null; 124 next.location.replace(url.href); 125 next.focus(); 126 document.dispatchEvent(new CustomEvent('poezdka:booking-search',{detail:{pageId:form.dataset.pzPageId,placement:event.submitter && event.submitter.form===form && !form.contains(event.submitter)?'article-end':'top'}})); 127 }); 128 controls.forEach(b=>b.disabled=false); 129 } 130 document.querySelectorAll('.pz-header-search input[type="search"]').forEach(input=>{ 131 input.setAttribute('aria-label',t('searchLabel'));input.placeholder=t('searchPlaceholder'); 132 }); 133 // Footer labels also remain owned by the localized CMS menu. 134 // Header labels belong to the CMS/menu and the language resolver. 135 // Never infer a link's label from its path: translations can share a path. 136});
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.