PageSourceSearch

https://www.dmitrov-city.com/assets/poezdka-journey.js?v=20261001-booking-new-tab

js dmitrov-city.com collected 2026-10-03 00:55:28 UTC 11,790 bytes, 136 lines download raw bytes

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.