1(function(){ 2 'use strict'; 3 const $ = (s, r=document) => r.querySelector(s); 4 const $$ = (s, r=document) => Array.from(r.querySelectorAll(s)); 5 6 const menuToggle = $('.menu-toggle'); 7 const mobileMenu = $('.mobile-menu'); 8 function setMobileMenu(open){ 9 document.documentElement.classList.toggle('menu-open', !!open); 10 document.body.classList.toggle('menu-open', !!open); 11 if(mobileMenu) mobileMenu.classList.toggle('is-open', !!open); 12 if(menuToggle){ 13 menuToggle.classList.toggle('is-open', !!open); 14 menuToggle.setAttribute('aria-expanded', open ? 'true' : 'false'); 15 } 16 } 17 function toggleMobileSubmenu(btn){ 18 const row = btn.closest('.mobile-parent-row') || btn; 19 const sub = row.nextElementSibling || btn.nextElementSibling; 20 const open = !btn.classList.contains('open'); 21 btn.classList.toggle('open', open); 22 row.classList.toggle('open', open); 23 btn.setAttribute('aria-expanded', open ? 'true' : 'false'); 24 if(sub && sub.classList.contains('mobile-sub')){ 25 sub.hidden = !open; 26 sub.classList.toggle('is-open', open); 27 } 28 } 29 if(menuToggle && mobileMenu){ 30 menuToggle.addEventListener('click', function(e){ 31 e.preventDefault(); 32 setMobileMenu(!document.body.classList.contains('menu-open')); 33 }); 34 $$('.mobile-menu a').forEach(function(link){ link.addEventListener('click', function(){ setMobileMenu(false); }); }); 35 $$('.mobile-parent-toggle, .mobile-parent').forEach(function(btn){ 36 btn.addEventListener('click', function(e){ 37 e.preventDefault(); 38 toggleMobileSubmenu(btn); 39 }); 40 }); 41 document.addEventListener('keydown', function(e){ 42 if(e.key === 'Escape' && document.body.classList.contains('menu-open')) setMobileMenu(false); 43 }); 44 } 45 46 // Persist the explicit language before navigation. Full-page cache can serve 47 // a prefixed URL without running PHP, so relying only on Set-Cookie response 48 // headers makes language switches intermittent. 49 function persistFrontendLanguage(lang){ 50 if(!/^(az|en|ru|ar)$/.test(lang)) return; 51 const secure = location.protocol === 'https:' ? '; Secure' : ''; 52 const common = '; path=/; max-age=31536000; SameSite=Lax' + secure; 53 const expired = '; path=/; max-age=0; SameSite=Lax' + secure; 54 const cookieDomain = location.hostname.indexOf('.') !== -1 && location.hostname !== 'localhost' 55 ? location.hostname.replace(/^www\./,'') 56 : ''; 57 function clearCookieVariants(name){ 58 document.cookie = name + '=; ' + expired.replace(/^; /, ''); 59 if(cookieDomain){ 60 document.cookie = name + '=; ' + expired.replace(/^; /, '') + '; domain=.' + cookieDomain; 61 } 62 } 63 ['sqv2_frontend_lang','mltlngg_language','mltlngg_current_language'].forEach(clearCookieVariants); 64 document.cookie = 'sqv2_frontend_lang=' + encodeURIComponent(lang) + common; 65 ['mltlngg_language','mltlngg_current_language'].forEach(function(name){ 66 const domain = cookieDomain ? '; domain=.' + cookieDomain : ''; 67 document.cookie = name + '=' + encodeURIComponent(lang) + common + domain; 68 }); 69 } 70 $$('.sqv2-lang-link[data-sqv2-language]').forEach(function(link){ 71 link.addEventListener('click', function(){ 72 persistFrontendLanguage(link.getAttribute('data-sqv2-language') || ''); 73 }); 74 }); 75 const explicitLanguage = location.pathname.split('/').filter(Boolean)[0] || ''; 76 if(/^(en|ru|ar)$/.test(explicitLanguage)) persistFrontendLanguage(explicitLanguage); 77 78 // V2 search overlay. Uses event delegation so desktop and mobile search 79 // buttons work even if markup is re-rendered by cache/minify plugins. 80 const searchOverlay = $('[data-search-overlay]'); 81 const searchClose = $('[data-search-close]'); 82 const searchInput = searchOverlay ? $('input[type="search"]', searchOverlay) : null; 83 let sqv2SearchScrollY = 0; 84 function lockSearchScroll(){ 85 sqv2SearchScrollY = window.scrollY || document.documentElement.scrollTop || 0; 86 document.documentElement.classList.add('sqv2-search-locked'); 87 document.body.classList.add('sqv2-search-open'); 88 document.body.style.position = 'fixed'; 89 document.body.style.top = '-' + sqv2SearchScrollY + 'px'; 90 document.body.style.left = '0'; 91 document.body.style.right = '0'; 92 document.body.style.width = '100%'; 93 } 94 function unlockSearchScroll(){ 95 document.documentElement.classList.remove('sqv2-search-locked'); 96 document.body.classList.remove('sqv2-search-open'); 97 document.body.style.position = ''; 98 document.body.style.top = ''; 99 document.body.style.left = ''; 100 document.body.style.right = ''; 101 document.body.style.width = ''; 102 window.scrollTo(0, sqv2SearchScrollY || 0); 103 } 104 function openSearch(){ 105 if(!searchOverlay) return; 106 searchOverlay.classList.add('is-open'); 107 searchOverlay.setAttribute('aria-hidden','false'); 108 lockSearchScroll(); 109 window.setTimeout(function(){ searchInput && searchInput.focus(); }, 80); 110 } 111 function closeSearch(){ 112 if(!searchOverlay) return; 113 searchOverlay.classList.remove('is-open'); 114 searchOverlay.setAttribute('aria-hidden','true'); 115 unlockSearchScroll(); 116 } 117 document.addEventListener('click', function(e){ 118 const openBtn = e.target.closest ? e.target.closest('[data-search-open]') : null; 119 if(openBtn){ e.preventDefault(); setMobileMenu(false); openSearch(); return; } 120 const closeBtn = e.target.closest ? e.target.closest('[data-search-close]') : null; 121 if(closeBtn){ e.preventDefault(); closeSearch(); } 122 }); 123 searchClose && searchClose.addEventListener('click', closeSearch); 124 searchOverlay && searchOverlay.addEventListener('click', function(e){ if(e.target === searchOverlay) closeSearch(); }); 125 document.addEventListener('keydown', function(e){ if(e.key === 'Escape') closeSearch(); }); 126 127 const hero = $('[data-hero]'); 128 if(hero){ 129 const slides = Array.isArray(window.SQV2_HERO_SLIDES) ? window.SQV2_HERO_SLIDES : []; 130 const bgWrap = $('.hero-bgs', hero); 131 const timeline = $('.timeline-wrap', hero); 132 const line = $('.timeline-line', hero); 133 const progress = $('.timeline-progress', hero); 134 const center = $('.hero-center', hero); 135 const counterNow = $('.counter-now', hero); 136 const total = $('.hero-counter b', hero); 137 const label = $('.hero-label', hero); 138 const year = $('.hero-year', hero); 139 const desc = $('.hero-desc', hero); 140 const cta = $('[data-hero-cta]', hero); 141 const prev = $('.side-prev', hero); 142 const next = $('.side-next', hero);
143 let current = 0; 144 let timer = null; 145 let paused = false; 146 147 if(total) total.textContent = String(slides.length || 1).padStart(2,'0'); 148 const bgNodes = $$('.hero-bg', hero); 149 function safeBgUrl(url){ 150 return String(url || '').replace(/"/g, '%22'); 151 } 152 function ensureHeroBg(i){ 153 const s = slides[i]; 154 const bg = bgNodes[i]; 155 if(!s || !s.img || !bg || bg.dataset.loaded === 'true') return; 156 bg.style.backgroundImage = 'url("' + safeBgUrl(s.img) + '")'; 157 bg.dataset.loaded = 'true'; 158 } 159 function warmAdjacentHeroBg(){ 160 if(!slides.length) return; 161 const nextIndex = (current + 1) % slides.length; 162 const run = function(){ ensureHeroBg(nextIndex); }; 163 // Do not compete with first paint/LCP on cold cache. The requested slide 164 // still loads immediately when the user navigates; this only prewarms the 165 // next one after the first page experience is stable. 166 window.setTimeout(function(){ 167 if('requestIdleCallback' in window) window.requestIdleCallback(run, { timeout: 1200 }); 168 else run(); 169 }, 5000); 170 } 171 172 slides.forEach(function(s, i){
173 let bg = bgNodes[i]; 174 if(!bg){ 175 bg = document.createElement('div'); 176 bgNodes[i] = bg; 177 bgWrap.appendChild(bg); 178 } 179 bg.className = 'hero-bg' + (i === 0 ? ' active' : ''); 180 if(i === 0) ensureHeroBg(i); 181 182 const node = document.createElement('button'); 183 const thumb = s.thumb || s.img; 184 node.type = 'button'; 185 node.className = 'timeline-node' + (i === 0 ? ' active' : ''); 186 node.innerHTML = '<span class="timeline-thumb"><img loading="lazy" decoding="async" width="220" height="147" src="'+thumb+'" alt="'+escapeHtml(s.year)+'"></span><span class="timeline-dot"></span><span class="timeline-year">'+escapeHtml(s.year)+'</span>'; 187 node.addEventListener('click', function(){ go(i); }); 188 timeline.appendChild(node); 189 }); 190 191 function escapeHtml(str){ 192 return String(str || '').replace(/[&<>"']/g, function(m){ return ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]); }); 193 } 194 function render(){ 195 if(!slides.length) return; 196 const s = slides[current]; 197 bgNodes.forEach(function(bg, i){ bg && bg.classList.toggle('active', i === current); });
198 $$('.timeline-node', hero).forEach(function(node, i){ node.classList.toggle('active', i === current); }); 199 if(counterNow) counterNow.textContent = String(current + 1).padStart(2,'0'); 200 if(label) label.textContent = s.label || ''; 201 if(year) year.textContent = s.title || s.year || ''; 202 if(desc) desc.textContent = s.desc || ''; 203 if(cta) cta.href = s.url || '#'; 204 const prevYear = slides[(current - 1 + slides.length) % slides.length].year; 205 const nextYear = slides[(current + 1) % slides.length].year; 206 if(prev){ 207 prev.textContent = prevYear; 208 prev.setAttribute('aria-label', prevYear + ' ' + (prev.getAttribute('data-a11y-label') || '')); 209 } 210 if(next){ 211 next.textContent = nextYear; 212 next.setAttribute('aria-label', nextYear + ' ' + (next.getAttribute('data-a11y-label') || '')); 213 } 214 updateTimelineLine(); 215 } 216 function updateTimelineLine(){ 217 if(!timeline) return; 218 const nodes = $$('.timeline-node', hero); 219 if(!nodes.length) return; 220 const first = nodes[0]; 221 const last = nodes[nodes.length - 1]; 222 const active = nodes[current] || first; 223 const firstCenter = first.offsetLeft + (first.offsetWidth / 2); 224 const lastCenter = last.offsetLeft + (last.offsetWidth / 2); 225 const activeCenter = active.offsetLeft + (active.offsetWidth / 2); 226 if(line){ 227 line.style.left = firstCenter + 'px'; 228 line.style.right = Math.max(0, timeline.clientWidth - lastCenter) + 'px'; 229 } 230 if(progress){ 231 progress.style.left = firstCenter + 'px'; 232 progress.style.width = Math.max(0, activeCenter - firstCenter) + 'px'; 233 } 234 } 235 function go(i){ 236 if(!slides.length || i === current) return; 237 ensureHeroBg((i + slides.length) % slides.length); 238 center && center.classList.add('is-changing'); 239 window.setTimeout(function(){ 240 current = (i + slides.length) % slides.length; 241 render(); 242 warmAdjacentHeroBg(); 243 center && center.classList.remove('is-changing'); 244 }, 180); 245 restart(); 246 } 247 function goNext(){ go(current + 1); } 248 function goPrev(){ go(current - 1); } 249 next && next.addEventListener('click', goNext); 250 prev && prev.addEventListener('click', goPrev); 251 hero.addEventListener('mouseenter', function(){ paused = true; }); 252 hero.addEventListener('mouseleave', function(){ paused = false; }); 253 document.addEventListener('keydown', function(e){ if(e.key === 'ArrowRight') goNext(); if(e.key === 'ArrowLeft') goPrev(); }); 254 window.addEventListener('resize', updateTimelineLine, { passive: true }); 255 let sx=0, sy=0, st=0; 256 hero.addEventListener('touchstart', function(e){ if(e.touches[0]){ sx=e.touches[0].clientX; sy=e.touches[0].clientY; st=Date.now(); } }, {passive:true}); 257 hero.addEventListener('touchend', function(e){ if(!e.changedTouches[0]) return; const dx=e.changedTouches[0].clientX-sx; const dy=e.changedTouches[0].clientY-sy; if(Math.abs(dx)>55 && Math.abs(dx)>Math.abs(dy) && Date.now()-st<700){ dx<0 ? goNext() : goPrev(); } }, {passive:true}); 258 function start(){ timer = window.setInterval(function(){ if(!paused) goNext(); }, 8000); } 259 function restart(){ window.clearInterval(timer); start(); } 260 render(); 261 warmAdjacentHeroBg(); 262 start(); 263 } 264 265 266 267 // Strict lazy images for below-the-fold editorial cards. Native lazy loading 268 // can still fetch nearby images during Lighthouse; this keeps large remote 269 // district images out of the initial network waterfall until they approach view. 270 (function(){ 271 const lazyImages = $$('img[data-sqv2-lazy-src]'); 272 if(!lazyImages.length) return; 273 function load(img){ 274 const src = img.getAttribute('data-sqv2-lazy-src'); 275 if(!src) return; 276 img.src = src; 277 img.removeAttribute('data-sqv2-lazy-src'); 278 img.classList.add('is-sqv2-lazy-loaded'); 279 } 280 if('IntersectionObserver' in window){ 281 const io = new IntersectionObserver(function(entries){ 282 entries.forEach(function(entry){ 283 if(entry.isIntersecting){ io.unobserve(entry.target); load(entry.target); } 284 }); 285 }, { rootMargin: '180px 0px' });
286 lazyImages.forEach(function(img){ io.observe(img); }); 287 } else { 288 lazyImages.forEach(load); 289 } 290 })(); 291 292 function scrollStrip(selector, dir){ 293 const strip = $(selector); 294 if(!strip) return; 295 const first = strip.children[0]; 296 const step = first ? first.getBoundingClientRect().width + 24 : 360; 297 strip.scrollBy({ left: step * dir, behavior: 'smooth' }); 298 } 299 $$('.news-next').forEach(btn => btn.addEventListener('click', () => scrollStrip('.news-airbnb-track', 1))); 300 $$('.news-prev').forEach(btn => btn.addEventListener('click', () => scrollStrip('.news-airbnb-track', -1))); 301 $$('.area-next').forEach(btn => btn.addEventListener('click', () => scrollStrip('.district-strip', 1))); 302 $$('.area-prev').forEach(btn => btn.addEventListener('click', () => scrollStrip('.district-strip', -1))); 303 304 $$('[data-article-slider]').forEach(function(slider){ 305 const slides = $$('.article-slide', slider); 306 const dots = $$('.article-slider-dot', slider); 307 const prev = $('.article-slider-prev', slider); 308 const next = $('.article-slider-next', slider); 309 const full = $('.article-slider-fullscreen', slider); 310 let index = 0; 311 function render(){ 312 slides.forEach((s,i)=>s.classList.toggle('is-active', i===index)); 313 dots.forEach((d,i)=>d.classList.toggle('is-active', i===index)); 314 if(dots[index] && dots[index].scrollIntoView){ 315 try { dots[index].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } catch(e) {} 316 } 317 } 318 function go(i){ index = (i + slides.length) % slides.length; render(); } 319 prev && prev.addEventListener('click', () => go(index-1)); 320 next && next.addEventListener('click', () => go(index+1)); 321 dots.forEach((dot,i)=>dot.addEventListener('click', () => go(i))); 322 if(full){ 323 full.addEventListener('click', function(){ 324 const isOpen = slider.classList.contains('is-fullscreen'); 325 slider.classList.toggle('is-fullscreen', !isOpen); 326 document.documentElement.classList.toggle('sqv2-slider-fullscreen-open', !isOpen); 327 document.body && document.body.classList.toggle('sqv2-slider-fullscreen-open', !isOpen); 328 full.setAttribute('aria-label', isOpen ? 'Fullscreen' : 'Close fullscreen'); 329 }); 330 document.addEventListener('keydown', function(e){ 331 if(e.key === 'Escape' && slider.classList.contains('is-fullscreen')){ 332 slider.classList.remove('is-fullscreen'); 333 document.documentElement.classList.remove('sqv2-slider-fullscreen-open'); 334 document.body && document.body.classList.remove('sqv2-slider-fullscreen-open'); 335 full.setAttribute('aria-label', 'Fullscreen'); 336 } 337 }); 338 } 339 render(); 340 }); 341 342 343 344 $$('[data-sqv2-article-viewer]').forEach(function(viewer){ 345 const slides = $$('.sqv2-article-media-viewer__slide', viewer); 346 const dots = $$('.sqv2-article-media-viewer__dot', viewer); 347 const prev = $('.sqv2-article-media-viewer__prev', viewer); 348 const next = $('.sqv2-article-media-viewer__next', viewer); 349 const full = $('.sqv2-article-media-viewer__fullscreen', viewer); 350 const close = $('.sqv2-article-media-viewer__modal-close', viewer); 351 const current = $('[data-sqv2-current]', viewer); 352 const progress = $('.sqv2-article-media-viewer__progress span', viewer); 353 let index = 0; 354 if (!slides.length) return; 355 356 function render(){ 357 slides.forEach((slide, i) => slide.classList.toggle('is-active', i === index)); 358 dots.forEach((dot, i) => dot.classList.toggle('is-active', i === index)); 359 if (current) current.textContent = String(index + 1); 360 if (progress) { 361 const percent = slides.length > 1 ? (((index + 1) / slides.length) * 100) : 100; 362 progress.style.setProperty('width', percent + '%', 'important'); 363 } 364 } 365 366 function go(nextIndex){ 367 index = (nextIndex + slides.length) % slides.length; 368 render(); 369 } 370 371 function setFullscreen(open){
372 viewer.classList.toggle('is-fullscreen', open); 373 document.documentElement.classList.toggle('sqv2-slider-fullscreen-open', open); 374 document.body && document.body.classList.toggle('sqv2-slider-fullscreen-open', open); 375 if (full) { 376 full.setAttribute('aria-label', open ? 'Close fullscreen' : 'Fullscreen'); 377 full.setAttribute('title', open ? 'Close fullscreen' : 'Fullscreen'); 378 full.setAttribute('aria-pressed', open ? 'true' : 'false'); 379 } 380 if (close) { 381 close.hidden = !open; 382 close.setAttribute('aria-hidden', open ? 'false' : 'true'); 383 } 384 } 385 386 prev && prev.addEventListener('click', () => go(index - 1)); 387 next && next.addEventListener('click', () => go(index + 1)); 388 dots.forEach((dot, i) => dot.addEventListener('click', () => go(i))); 389 full && full.addEventListener('click', () => setFullscreen(!viewer.classList.contains('is-fullscreen'))); 390 close && close.addEventListener('click', () => setFullscreen(false)); 391 392 viewer.addEventListener('click', function(e){ 393 if (!viewer.classList.contains('is-fullscreen')) return; 394 if (e.target === viewer) setFullscreen(false); 395 }); 396 397 document.addEventListener('keydown', function(e){ 398 if (e.key === 'Escape' && viewer.classList.contains('is-fullscreen')) { 399 setFullscreen(false); 400 } 401 if (!viewer.classList.contains('is-fullscreen') || slides.length < 2) return; 402 if (e.key === 'ArrowLeft') go(index - 1); 403 if (e.key === 'ArrowRight') go(index + 1); 404 }); 405 406 render(); 407 }); 408 409 function sqv2CurrentShareUrl(){ 410 try { 411 const url = new URL(window.location.href); 412 url.hash = ''; 413 return url.toString(); 414 } catch(e) { 415 return window.location.href.split('#')[0]; 416 } 417 } 418 function sqv2FacebookShareHref(url){ 419 return 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url); 420 } 421 const currentShareUrl = sqv2CurrentShareUrl(); 422 $$('.share-rail').forEach(function(rail){ 423 const facebook = $('a[href*="facebook.com/sharer"]', rail); 424 if(facebook) facebook.setAttribute('href', sqv2FacebookShareHref(currentShareUrl)); 425 $$('.sqv2-copy-link', rail).forEach(function(btn){ 426 btn.setAttribute('data-copy', currentShareUrl); 427 }); 428 }); 429 $$('.sqv2-copy-link').forEach(function(btn){ 430 btn.addEventListener('click', function(){ 431 const value = btn.getAttribute('data-copy') || sqv2CurrentShareUrl(); 432 if(navigator.share){ 433 try { navigator.share({title: document.title || '', url: value}).catch(function(){}); } catch(err) {} 434 } 435 if(navigator.clipboard){ navigator.clipboard.writeText(value).catch(function(){}); } 436 btn.classList.add('copied'); 437 window.setTimeout(() => btn.classList.remove('copied'), 1200); 438 }); 439 }); 440})(); 441 442(function(){ 443 'use strict'; 444 const cfg = window.SQV2_SEARCH || null; 445 if(!cfg) return; 446 const input = document.querySelector('[data-sqv2-search-input]'); 447 const form = document.querySelector('[data-sqv2-search-form]'); 448 const suggestions = document.querySelector('[data-sqv2-search-suggestions]'); 449 if(form){ 450 form.addEventListener('submit', function(e){ 451 const q = input ? input.value.trim() : ''; 452 if(!q) return; 453 e.preventDefault(); 454 const base = (cfg.searchUrl || form.getAttribute('action') || window.location.pathname).replace(/[?#].*$/, ''); 455 window.location.href = base + '?s=' + encodeURIComponent(q); 456 }); 457 } 458 // Do not block the native GET form submit. This makes search work even 459 // when AJAX suggestions are disabled or blocked by cache/security plugins. 460 let timer = null; 461 function render(items){ 462 if(!suggestions) return; 463 suggestions.innerHTML = ''; 464 const max = Math.max(1, parseInt(cfg.maxSuggestions || 10, 10)); 465 if(!items || !items.length){ 466 if(input && input.value.trim().length > 1){ 467 const empty = document.createElement('div'); 468 empty.className = 'sqv2-search-suggestion sqv2-search-suggestion-empty'; 469 empty.innerHTML = '<strong>'+escapeHtml(cfg.noResults || 'No results')+'</strong>'; 470 suggestions.appendChild(empty); 471 } 472 return; 473 }
474 items.slice(0, max).forEach(function(item){ 475 const a = document.createElement('a'); 476 a.className = 'sqv2-search-suggestion'; 477 a.href = item.url; 478 const meta = item.date || item.type || ''; 479 a.innerHTML = '<strong>'+escapeHtml(item.title)+'</strong>' + (meta ? '<span>'+escapeHtml(meta)+'</span>' : ''); 480 suggestions.appendChild(a); 481 }); 482 } 483 function escapeHtml(str){ return String(str || '').replace(/[&<>"']/g, function(m){ return ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]); }); } 484 function normalizeSearch(str){ 485 return String(str || '') 486 .toLocaleLowerCase('az') 487 .normalize('NFKD') 488 .replace(/[\u0300-\u036f]/g, '') 489 .replace(/[ı]/g, 'i') 490 .replace(/[É]/g, 'e') 491 .replace(/[ö]/g, 'o') 492 .replace(/[ü]/g, 'u') 493 .replace(/[Ä]/g, 'g') 494 .replace(/[Å]/g, 's') 495 .replace(/[ç]/g, 'c') 496 .replace(/\s+/g, ' ') 497 .trim(); 498 } 499 function localIndexItems(q){ 500 const index = Array.isArray(cfg.index) ? cfg.index : []; 501 if(!index.length) return []; 502 const max = Math.max(1, parseInt(cfg.maxSuggestions || 10, 10));
503 const terms = normalizeSearch(q).split(' ').filter(Boolean); 504 if(!terms.length) return []; 505 const scored = []; 506 index.forEach(function(item, originalIndex){ 507 const title = normalizeSearch(item.title || ''); 508 const haystack = normalizeSearch((item.search || '') + ' ' + (item.title || '') + ' ' + (item.date || '') + ' ' + (item.type || '')); 509 let score = 0; 510 for(let i = 0; i < terms.length; i++){ 511 const term = terms[i]; 512 if(!haystack.includes(term)) return; 513 if(title === term) score += 220; 514 else if(title.startsWith(term)) score += 160; 515 else if(title.includes(term)) score += 110; 516 else score += 40; 517 } 518 scored.push({item:item, score:score, originalIndex:originalIndex}); 519 }); 520 scored.sort(function(a,b){ return (b.score - a.score) || (a.originalIndex - b.originalIndex); }); 521 return scored.slice(0, max).map(function(row){ return row.item; }); 522 } 523 if(input){ 524 input.addEventListener('input', function(){ 525 const q = input.value.trim(); 526 window.clearTimeout(timer); 527 if(q.length < 2){ render([]); return; } 528 529 const localItems = localIndexItems(q); 530 render(localItems); 531 if(localItems.length >= Math.max(1, parseInt(cfg.maxSuggestions || 10, 10))){ return; } 532 533 timer = window.setTimeout(function(){ 534 if(!window.fetch){ return; } 535 const lang = encodeURIComponent(cfg.lang || 'az'); 536 const ajaxUrl = cfg.ajax + '?action=sqv2_search_suggest&lang=' + lang + '&q=' + encodeURIComponent(q); 537 const suggestBase = (cfg.suggestUrl || cfg.searchUrl || window.location.pathname).replace(/[?#].*$/, ''); 538 const fallbackJsonUrl = suggestBase + '?sqv2_suggest=1&lang=' + lang + '&s=' + encodeURIComponent(q); 539 function requestText(url, accept){ 540 return fetch(url, {credentials:'same-origin', headers:{'Accept': accept || 'application/json'}}).then(function(r){ 541 if(!r.ok) throw new Error('bad_status'); 542 return r.text(); 543 }); 544 } 545 function requestJson(url){ 546 return requestText(url, 'application/json').then(function(text){ 547 var json; 548 try { json = JSON.parse(text); } catch(e) { throw new Error('bad_json'); } 549 if(json && json.success && json.data) return json.data.items || []; 550 if(json && json.data && Array.isArray(json.data.items)) return json.data.items; 551 if(json && Array.isArray(json.items)) return json.items; 552 return []; 553 }); 554 } 555 requestJson(ajaxUrl) 556 .catch(function(){ return requestJson(fallbackJsonUrl); }) 557 .then(function(remoteItems){ 558 const merged = []; 559 const seen = Object.create(null); 560 (remoteItems || []).concat(localItems || []).forEach(function(item){ 561 const key = String(item && item.url || '') || String(item && item.title || ''); 562 if(!key || seen[key]) return; 563 seen[key] = true; 564 merged.push(item); 565 }); 566 render(merged.length ? merged : localItems); 567 }) 568 .catch(function(){ render(localItems); }); 569 }, 180); 570 }); 571 } 572})(); 573 574(function(){ 575 'use strict'; 576 function qs(s, r){ return (r || document).querySelector(s); } 577 function isNewsAjaxLink(a){ 578 return a && a.href && a.closest && a.closest('.sqv2-news-page') && (a.closest('.sqv2-news-filter-tabs') || a.closest('.sqv2-pagination')); 579 } 580 function swapNewsContent(url, push){ 581 var page = qs('.sqv2-news-page'); 582 var container = page && qs('.container', page); 583 if(!page || !container || !window.fetch || !window.DOMParser){ window.location.href = url; return; } 584 page.classList.add('is-filter-loading'); 585 fetch(url, {credentials:'same-origin', headers:{'X-Requested-With':'XMLHttpRequest'}}) 586 .then(function(r){ if(!r.ok) throw new Error('bad_status'); return r.text(); }) 587 .then(function(html){ 588 var doc = new DOMParser().parseFromString(html, 'text/html'); 589 var nextPage = qs('.sqv2-news-page', doc); 590 var nextContainer = nextPage && qs('.container', nextPage); 591 if(!nextContainer) throw new Error('missing_news'); 592 container.innerHTML = nextContainer.innerHTML; 593 if(push !== false) history.pushState({sqv2News:true}, '', url); 594 var hero = qs('.sqv2-news-hero') || qs('.page-hero'); 595 if(hero) hero.scrollIntoView({block:'start', behavior:'smooth'}); 596 }) 597 .catch(function(){ window.location.href = url; }) 598 .finally(function(){ page.classList.remove('is-filter-loading'); }); 599 } 600 document.addEventListener('click', function(e){ 601 var a = e.target && e.target.closest ? e.target.closest('a') : null; 602 if(!isNewsAjaxLink(a)) return; 603 if(e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || a.target) return; 604 e.preventDefault(); 605 if(a.classList.contains('chip')){ 606 var wrap = a.closest('.sqv2-news-filter-tabs'); 607 if(wrap){ Array.prototype.forEach.call(wrap.querySelectorAll('.chip'), function(x){ x.classList.remove('active'); }); a.classList.add('active'); } 608 } 609 swapNewsContent(a.href, true); 610 }); 611 window.addEventListener('popstate', function(){ if(qs('.sqv2-news-page')) swapNewsContent(window.location.href, false); }); 612 // Step 50: robust directions chooser and stable Azerbaijani date/time fields. 613 (function(){ 614 function qs2(sel, root){ return (root || document).querySelector(sel); } 615 function qsa2(sel, root){ return Array.prototype.slice.call((root || document).querySelectorAll(sel)); } 616 function isMobileLike(){ 617 return (window.matchMedia && window.matchMedia('(max-width: 900px)').matches) || (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) || ('ontouchstart' in window); 618 } 619 function openMapSheet(trigger){ 620 var sheet = qs2('[data-sqv2-map-choice]'); 621 if(!sheet) return false;
622 sheet.hidden = false; 623 sheet.classList.add('is-open'); 624 sheet.setAttribute('aria-hidden','false'); 625 document.body.classList.add('sqv2-map-choice-open'); 626 qsa2('a', sheet).forEach(function(a){ 627 var key = a.getAttribute('data-map-app'); 628 if(key === 'google' && trigger.getAttribute('data-google-url')) a.href = trigger.getAttribute('data-google-url'); 629 if(key === 'waze' && trigger.getAttribute('data-waze-url')) a.href = trigger.getAttribute('data-waze-url'); 630 if(key === 'apple' && trigger.getAttribute('data-apple-url')) a.href = trigger.getAttribute('data-apple-url'); 631 }); 632 return true; 633 } 634 function closeMapSheet(){ 635 var sheet = qs2('[data-sqv2-map-choice]'); 636 if(!sheet) return; 637 sheet.classList.remove('is-open'); 638 sheet.setAttribute('aria-hidden','true'); 639 document.body.classList.remove('sqv2-map-choice-open'); 640 window.setTimeout(function(){ if(!sheet.classList.contains('is-open')) sheet.hidden = true; }, 120); 641 } 642 function handleDirectionsEvent(e){ 643 var closeBtn = e.target && e.target.closest ? e.target.closest('[data-sqv2-directions-close]') : null; 644 if(closeBtn){ e.preventDefault(); closeMapSheet(); return; } 645 var sheet = qs2('[data-sqv2-map-choice]'); 646 if(sheet && e.target === sheet){ e.preventDefault(); closeMapSheet(); return; } 647 var trigger = e.target && e.target.closest ? e.target.closest('[data-sqv2-directions-open]') : null; 648 if(!trigger) return; 649 e.preventDefault(); 650 if(e.stopPropagation) e.stopPropagation(); 651 if(isMobileLike() && openMapSheet(trigger)) return; 652 var url = trigger.getAttribute('data-google-url') || trigger.getAttribute('href'); 653 if(url) window.open(url, '_blank', 'noopener'); 654 } 655 document.addEventListener('click', handleDirectionsEvent, true); 656 document.addEventListener('keydown', function(e){ 657 if(e.key === 'Escape') closeMapSheet(); 658 var trigger = e.target && e.target.closest ? e.target.closest('[data-sqv2-directions-open]') : null; 659 if(!trigger || (e.key !== 'Enter' && e.key !== ' ')) return; 660 e.preventDefault(); 661 if(isMobileLike() && openMapSheet(trigger)) return; 662 var url = trigger.getAttribute('data-google-url') || trigger.getAttribute('href'); 663 if(url) window.open(url, '_blank', 'noopener'); 664 }, true); 665 666 function digits(v){ return String(v || '').replace(/\D+/g, ''); } 667 function pad2(v){ v = String(v || ''); return v.length === 1 ? '0' + v : v; } 668 function fullYear(y){ y = String(y || ''); return y.length === 2 ? '20' + y : y.slice(0, 4); } 669 function dateTextFromIso(v){ 670 var m = String(v || '').match(/^(\d{4})-(\d{2})-(\d{2})$/); 671 return m ? (m[3] + '.' + m[2] + '.' + m[1]) : ''; 672 } 673 function normalizeDateLive(v){ 674 var raw = String(v || '').trim().replace(/[\/\-]/g, '.').replace(/[^0-9.]/g, ''); 675 if(!raw) return ''; 676 if(raw.indexOf('.') !== -1){ 677 var trailingDot = raw.slice(-1) === '.'; 678 var parts = raw.split('.'); 679 var day = (parts[0] || '').replace(/\D/g, '').slice(0, 2); 680 var month = (parts[1] || '').replace(/\D/g, '').slice(0, 2); 681 var year = (parts.slice(2).join('') || '').replace(/\D/g, '').slice(0, 4); 682 var out = day; 683 if(parts.length > 1) out += '.' + month; 684 if(parts.length > 2) out += '.' + year; 685 if(trailingDot && out.slice(-1) !== '.') out += '.'; 686 return out.slice(0, 10); 687 } 688 var d = digits(raw).slice(0, 8); 689 if(!d) return ''; 690 if(d.length <= 2) return d; 691 if(d.length <= 4) return d.slice(0, 2) + '.' + d.slice(2); 692 return d.slice(0, 2) + '.' + d.slice(2, 4) + '.' + d.slice(4); 693 } 694 function normalizeDateFinal(v){ 695 var raw = String(v || '').trim(); 696 var p = raw.match(/^(\d{1,2})[.\-/](\d{1,2})[.\-/](\d{2}|\d{4})$/); 697 if(p) return pad2(p[1]).slice(-2) + '.' + pad2(p[2]).slice(-2) + '.' + fullYear(p[3]); 698 var d = digits(raw).slice(0, 8); 699 if(!d) return ''; 700 if(d.length <= 2) return d; 701 if(d.length <= 4) return d.slice(0, 2) + '.' + d.slice(2); 702 var day = d.slice(0, 2);
703 var month = d.slice(2, 4); 704 var year = d.slice(4); 705 if(year.length === 2) year = '20' + year; 706 return day + '.' + month + '.' + year; 707 } 708 function isoFromDateText(v){ 709 var normalized = normalizeDateFinal(v); 710 var m = normalized.match(/^(\d{2})\.(\d{2})\.(\d{4})$/); 711 return m ? (m[3] + '-' + m[2] + '-' + m[1]) : ''; 712 } 713 function normalizeTimeLive(v){ 714 var raw = String(v || '').trim().replace(/[.\-]/g, ':').replace(/[^0-9:]/g, ''); 715 if(!raw) return ''; 716 if(raw.indexOf(':') !== -1){ 717 var trailingColon = raw.slice(-1) === ':'; 718 var parts = raw.split(':'); 719 var h = (parts[0] || '').replace(/\D/g, '').slice(0, 2); 720 var m = (parts.slice(1).join('') || '').replace(/\D/g, '').slice(0, 2); 721 return h + ':' + m + (trailingColon && m === '' ? '' : ''); 722 } 723 var d = digits(raw).slice(0, 4); 724 if(!d) return ''; 725 if(d.length <= 2) return d; 726 return d.slice(0, 2) + ':' + d.slice(2); 727 } 728 function normalizeTimeFinal(v){ 729 var raw = String(v || '').trim(); 730 var h = '', m = ''; 731 var p = raw.match(/^(\d{1,2})[:.\-](\d{0,2})$/); 732 var d = digits(raw).slice(0, 4); 733 if(p){ h = p[1]; m = p[2] || '00'; } 734 else if(d.length <= 2){ h = d; m = '00'; } 735 else { h = d.slice(0, 2); m = d.slice(2, 4); } 736 if(!h) return ''; 737 var hh = Math.min(23, Math.max(0, parseInt(h || '0', 10))); 738 var mm = Math.min(59, Math.max(0, parseInt(m || '0', 10))); 739 return pad2(hh) + ':' + pad2(mm); 740 } 741 function syncDateText(input, final){ 742 if(!input) return; 743 var before = input.value; 744 var after = final ? normalizeDateFinal(before) : normalizeDateLive(before); 745 if(before !== after){ input.value = after; try{ input.setSelectionRange(after.length, after.length); }catch(e){} } 746 var native = input.closest('.sqv2-picker-field') && input.closest('.sqv2-picker-field').querySelector('[data-sqv2-native-date]'); 747 var iso = isoFromDateText(input.value); 748 if(native && iso) native.value = iso; 749 } 750 function syncTimeText(input, final){ 751 if(!input) return; 752 var before = input.value; 753 var after = final ? normalizeTimeFinal(before) : normalizeTimeLive(before); 754 if(before !== after){ input.value = after; try{ input.setSelectionRange(after.length, after.length); }catch(e){} } 755 } 756 function openNativePicker(native){ 757 if(!native) return; 758 try { if(native.showPicker){ native.showPicker(); return; } } catch(err) {} 759 try { native.focus(); native.click(); } catch(err2) {} 760 } 761 // Do not live-format while the user is typing. Live masking can swallow 762 // zeros such as 16:00 in some mobile/desktop browsers. Final formatting 763 // runs on blur/change/submit only. 764 document.addEventListener('blur', function(e){ 765 if(e.target && e.target.matches('[data-sqv2-date-az]')) syncDateText(e.target, true); 766 if(e.target && e.target.matches('[data-sqv2-time-az], [data-sqv2-time-stable]')) syncTimeText(e.target, true); 767 }, true); 768 document.addEventListener('change', function(e){ 769 if(e.target && e.target.matches('[data-sqv2-native-date]')){ 770 var textDate = e.target.closest('.sqv2-picker-field') && e.target.closest('.sqv2-picker-field').querySelector('[data-sqv2-date-az]'); 771 if(textDate) textDate.value = dateTextFromIso(e.target.value); 772 } 773 if(e.target && e.target.matches('[data-sqv2-date-az]')) syncDateText(e.target, true); 774 if(e.target && e.target.matches('[data-sqv2-time-az], [data-sqv2-time-stable]')) syncTimeText(e.target, true); 775 }, true); 776 document.addEventListener('paste', function(e){ 777 if(e.target && e.target.matches('[data-sqv2-date-az], [data-sqv2-time-az], [data-sqv2-time-stable]')){ 778 window.setTimeout(function(){ 779 if(e.target.matches('[data-sqv2-date-az]')) syncDateText(e.target, true); 780 if(e.target.matches('[data-sqv2-time-az], [data-sqv2-time-stable]')) syncTimeText(e.target, true); 781 }, 0); 782 } 783 }, true); 784 document.addEventListener('click', function(e){ 785 var dateBtn = e.target && e.target.closest ? e.target.closest('[data-sqv2-open-date]') : null; 786 var timeBtn = e.target && e.target.closest ? e.target.closest('[data-sqv2-open-time]') : null; 787 if(dateBtn){ 788 e.preventDefault(); e.stopPropagation(); 789 var wrap = dateBtn.closest('.sqv2-picker-field'); 790 var text = wrap && wrap.querySelector('[data-sqv2-date-az]'); 791 var native = wrap && wrap.querySelector('[data-sqv2-native-date]'); 792 if(text) syncDateText(text, true); 793 if(native && text){ var iso = isoFromDateText(text.value); if(iso) native.value = iso; } 794 openNativePicker(native); 795 } 796 if(timeBtn){ 797 e.preventDefault(); e.stopPropagation(); 798 var wrap2 = timeBtn.closest('.sqv2-picker-field'); 799 var text2 = wrap2 && wrap2.querySelector('[data-sqv2-time-az], [data-sqv2-time-stable]'); 800 var menu = wrap2 && wrap2.querySelector('[data-sqv2-time-menu]'); 801 if(menu){ 802 var open = menu.hasAttribute('hidden'); 803 qsa2('[data-sqv2-time-menu]').forEach(function(m){ m.hidden = true; m.classList.remove('is-open'); }); 804 if(open){ menu.hidden = false; menu.classList.add('is-open'); } 805 } 806 if(text2){ text2.focus(); } 807 } 808 }, true); 809 document.addEventListener('click', function(e){ 810 var opt = e.target && e.target.closest ? e.target.closest('[data-sqv2-time-option]') : null; 811 if(opt){ 812 e.preventDefault(); e.stopPropagation(); 813 var wrap = opt.closest('.sqv2-picker-field'); 814 var input = wrap && wrap.querySelector('[data-sqv2-time-az], [data-sqv2-time-stable]'); 815 if(input){ input.value = opt.getAttribute('value') || opt.textContent.trim(); input.focus(); } 816 var menu = wrap && wrap.querySelector('[data-sqv2-time-menu]'); 817 if(menu){ menu.hidden = true; menu.classList.remove('is-open'); } 818 return; 819 } 820 if(!e.target.closest || !e.target.closest('.sqv2-picker-field')){ 821 qsa2('[data-sqv2-time-menu]').forEach(function(m){ m.hidden = true; m.classList.remove('is-open'); }); 822 } 823 }, true); 824 document.addEventListener('submit', function(e){ 825 var form = e.target && e.target.closest ? e.target.closest('.sqv2-tour-form') : null; 826 if(!form) return; 827 qsa2('[data-sqv2-date-az]', form).forEach(function(input){ syncDateText(input, true); }); 828 qsa2('[data-sqv2-time-az], [data-sqv2-time-stable]', form).forEach(function(input){ syncTimeText(input, true); }); 829 }, true); 830 })(); 831 832 833 834 /* Step 53: make district detail chips scroll reliably on touch devices. */ 835 (function(){ 836 function rows(){ return Array.prototype.slice.call(document.querySelectorAll('.district-side-nav-scroll')); } 837 function prepare(row){ 838 if(!row || row.__sqv2ChipScrollReady) return; 839 row.__sqv2ChipScrollReady = true; 840 row.setAttribute('data-sqv2-chip-scroll','true'); 841 var active = row.querySelector('a.active'); 842 if(active && active.scrollIntoView){ 843 window.setTimeout(function(){ 844 try{ active.scrollIntoView({behavior:'smooth', block:'nearest', inline:'center'}); }catch(e){} 845 }, 150); 846 } 847 var dragging = false;
848 var moved = false; 849 var startX = 0; 850 var startLeft = 0; 851 function endDrag(e){ 852 if(!dragging) return; 853 dragging = false; 854 row.classList.remove('is-dragging'); 855 try{ if(e && e.pointerId != null) row.releasePointerCapture(e.pointerId); }catch(err){} 856 window.setTimeout(function(){ moved = false; }, 80); 857 } 858 row.addEventListener('pointerdown', function(e){ 859 if(e.pointerType === 'mouse' && e.button !== 0) return; 860 if(row.scrollWidth <= row.clientWidth + 2) return; 861 dragging = true; 862 moved = false; 863 startX = e.clientX; 864 startLeft = row.scrollLeft; 865 row.classList.add('is-dragging'); 866 try{ row.setPointerCapture(e.pointerId); }catch(err){} 867 }, {passive:true}); 868 row.addEventListener('pointermove', function(e){ 869 if(!dragging) return; 870 var dx = e.clientX - startX; 871 if(Math.abs(dx) > 3){ 872 moved = true; 873 row.scrollLeft = startLeft - dx; 874 if(e.cancelable) e.preventDefault(); 875 } 876 }, {passive:false}); 877 row.addEventListener('pointerup', endDrag, {passive:true}); 878 row.addEventListener('pointercancel', endDrag, {passive:true}); 879 row.addEventListener('mouseleave', endDrag, {passive:true}); 880 row.addEventListener('click', function(e){ 881 if(moved){ 882 e.preventDefault(); 883 e.stopPropagation(); 884 if(e.stopImmediatePropagation) e.stopImmediatePropagation(); 885 } 886 }, true); 887 } 888 function init(){ rows().forEach(prepare); } 889 if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); 890 window.addEventListener('resize', function(){ rows().forEach(function(row){ row.style.overflowX = 'auto'; }); }, {passive:true}); 891 })(); 892 893 894 /* Step 54: robust native + drag horizontal scroll for district chips and hero timeline. */ 895 (function(){ 896 function toArray(list){ return Array.prototype.slice.call(list || []); } 897 function scrollers(){ 898 return toArray(document.querySelectorAll('.district-side-nav, .district-side-nav-scroll, .hero-timeline, .hero-timeline .timeline-wrap')); 899 } 900 function isScrollable(el){ return el && el.scrollWidth > el.clientWidth + 4; } 901 function prepare(el){ 902 if(!el || el.__sqv2HScroll54) return; 903 el.__sqv2HScroll54 = true; 904 var dragging = false; 905 var moved = false; 906 var startX = 0; 907 var startLeft = 0; 908 function start(e){ 909 if(e.pointerType === 'mouse' && e.button !== 0) return; 910 if(!isScrollable(el)) return; 911 dragging = true; 912 moved = false; 913 startX = e.clientX; 914 startLeft = el.scrollLeft; 915 el.classList.add('is-dragging'); 916 try{ el.setPointerCapture(e.pointerId); }catch(err){} 917 if(e.stopPropagation) e.stopPropagation(); 918 } 919 function move(e){ 920 if(!dragging) return; 921 var dx = e.clientX - startX; 922 if(Math.abs(dx) > 2){ 923 moved = true; 924 el.scrollLeft = startLeft - dx; 925 if(e.cancelable) e.preventDefault(); 926 if(e.stopPropagation) e.stopPropagation(); 927 } 928 } 929 function end(e){ 930 if(!dragging) return; 931 dragging = false; 932 el.classList.remove('is-dragging'); 933 try{ if(e && e.pointerId != null) el.releasePointerCapture(e.pointerId); }catch(err){} 934 window.setTimeout(function(){ moved = false; }, 90); 935 if(e && e.stopPropagation) e.stopPropagation(); 936 } 937 el.addEventListener('pointerdown', start, {passive:true}); 938 el.addEventListener('pointermove', move, {passive:false}); 939 el.addEventListener('pointerup', end, {passive:true}); 940 el.addEventListener('pointercancel', end, {passive:true}); 941 el.addEventListener('mouseleave', end, {passive:true}); 942 el.addEventListener('click', function(e){ 943 if(moved){ 944 e.preventDefault(); 945 e.stopPropagation(); 946 if(e.stopImmediatePropagation) e.stopImmediatePropagation(); 947 } 948 }, true); 949 ['touchstart','touchmove','touchend'].forEach(function(type){ 950 el.addEventListener(type, function(e){ if(isScrollable(el) && e.stopPropagation) e.stopPropagation(); }, {passive:true}); 951 }); 952 } 953 function centerActive(){ 954 toArray(document.querySelectorAll('.district-side-nav-scroll a.active, .hero-timeline .timeline-node.active')).forEac
954h(function(active){ 955 var parent = active.closest('.district-side-nav') || active.closest('.district-side-nav-scroll') || active.closest('.hero-timeline') || active.parentElement; 956 if(parent && isScrollable(parent)){ 957 var left = active.offsetLeft - (parent.clientWidth - active.offsetWidth) / 2; 958 try{ parent.scrollTo({left:Math.max(0,left), behavior:'smooth'}); }catch(e){ parent.scrollLeft = Math.max(0,left); } 959 } 960 }); 961 } 962 function init(){ scrollers().forEach(prepare); window.setTimeout(centerActive, 180); } 963 if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); 964 window.addEventListener('resize', function(){ window.setTimeout(init, 60); }, {passive:true}); 965 })(); 966 967})();
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.