1/* Auto-generated by scripts/build-search-bar-components-bundle.mjs. Do not edit manually. */ 2 3// ---- START: js/components/mirela-nb-seo-slugs-runtime.js ---- 4/** 5 * New-buildings list SEO city-segment helpers. Prefer window.__mirelaNbSeoCitySlugs from SSR (search-bar-scripts). 6 */ 7(function (window) { 8 'use strict'; 9 10 var FALLBACK = ['novi-sgradi-v-stroej', 'new-buildings-under-construction', 'novye-zdaniya-v-stroitelstve', 'neubauten-im-bau']; 11 12 function slugSet() { 13 try { 14 var s = window.__mirelaNbSeoCitySlugs; 15 if (Array.isArray(s) && s.length) return s.map(function (x) { return String(x || '').toLowerCase().trim(); }).filter(Boolean); 16 } catch (_e) {} 17 return FALLBACK.slice(); 18 } 19 20 if (typeof window.__mirelaIsNbSeoCitySlug !== 'function') { 21 window.__mirelaIsNbSeoCitySlug = function (seg) { 22 var x = String(seg == null ? '' : seg).toLowerCase().trim(); 23 if (!x) return false; 24 var a = slugSet(); 25 return a.indexOf(x) !== -1; 26 }; 27 } 28 29 if (typeof window.__mirelaPathnameHasNbSeoCity !== 'function') { 30 window.__mirelaPathnameHasNbSeoCity = function (pathname) { 31 try { 32 var ru = window.MirelaSearch && window.MirelaSearch.routeUtils; 33 var parts = (ru && typeof ru.getPathSegments === 'function') 34 ? ru.getPathSegments(pathname || '') 35 : String(pathname || '').replace(/^\/+/, '').split('/').filter(Boolean); 36 var hasLoc = !!(parts.length && ru && typeof ru.isLocaleSegment === 'function' && ru.isLocaleSegment(parts[0])); 37 var dealIdx = hasLoc ? 1 : 0; 38 var citySeg = String(parts[dealIdx + 1] || '').trim(); 39 return window.__mirelaIsNbSeoCitySlug(citySeg); 40 } catch (_e2) { 41 return false; 42 } 43 }; 44 } 45})(window); 46 47// ---- END: js/components/mirela-nb-seo-slugs-runtime.js ---- 48 49// ---- START: js/components/mirela-search-legacy-bridge.js ---- 50/* Legacy global bridge for search helpers. */ 51(function (window, document) { 52 'use strict'; 53 54 var root = window.MirelaSearch = window.MirelaSearch || {}; 55 var legacy = root.legacy = root.legacy || {}; 56 57 function getSharedApi() { 58 try { return window.AdvancedSearchShared || null; } catch (_e) { return null; } 59 } 60 61 function getFromShared(methodName) { 62 var shared = getSharedApi(); 63 if (shared && typeof shared[methodName] === 'function') return shared[methodName].bind(shared); 64 return null; 65 } 66 67 legacy.getSharedApi = function () { return getSharedApi(); }; 68 69 legacy.q = function (selector) { 70 try { 71 var fn = getFromShared('selectorGet'); 72 if (fn) return fn(selector); 73 } catch (_e) {} 74 try { return document.querySelector(selector); } catch (_e2) { return null; } 75 }; 76 77 legacy.ribbonFilterCapture = function (fromInp, toInp, sanitize) { 78 try { 79 var fn = getFromShared('rangeCapture'); 80 if (fn) return fn(fromInp, toInp, sanitize); 81 } catch (_e) {} 82 return { 83 from: String((sanitize && sanitize(fromInp && fromInp.value)) || ''), 84 to: String((sanitize && sanitize(toInp && toInp.value)) || '') 85 }; 86 }; 87 88 legacy.normalizeRibbonRange = function (opts) { 89 try { 90 var fn = getFromShared('rangeNormalize'); 91 if (fn) return fn(opts || {}); 92 } catch (_e) {} 93 opts = opts || {}; 94 var fromInp = opts.fromInp || null; 95 var toInp = opts.toInp || null; 96 var sanitize = opts.sanitize; 97 var mirrorTo = Array.isArray(opts.mirrorToInputs) ? opts.mirrorToInputs : []; 98 var minGap = opts.minGap; 99 function valOf(inp) { 100 try { 101 if (sanitize) return sanitize((inp && inp.value) || ''); 102 return ((inp && inp.value) || '').toString().trim(); 103 } catch (_e2) { return ''; } 104 } 105 function setMirrorToInputs(str) { 106 mirrorTo.forEach(function (inp) { try { if (inp) inp.value = str; } catch (_e4) {} }); 107 } 108 var f = valOf(fromInp); 109 var t = valOf(toInp); 110 if (!f || !t) { 111 return { from: String(f || ''), to: String(t || ''), changed: false }; 112 } 113 var fnn = parseInt(String(f || ''), 10) || 0; 114 var tnn = parseInt(String(t || ''), 10) || 0; 115 var narrow = false; 116 try { 117 narrow = typeof window !== 'undefined' && window.innerWidth < 1050; 118 } catch (_eW) {} 119 if (typeof minGap === 'number' && minGap >= 0) { 120 if (narrow) { 121 if (tnn < fnn) { 122 try { if (toInp) toInp.value = ''; } catch (_e3) {} 123 setMirrorToInputs(''); 124 return { from: String(f || ''), to: '', changed: true }; 125 } 126 } else if (tnn < fnn + minGap) { 127 var bumped = fnn + minGap; 128 var bumpedStr = String(bumped); 129 try { if (toInp) toInp.value = bumpedStr; } catch (_e3b) {} 130 setMirrorToInputs(bumpedStr); 131 return { from: String(f || ''), to: bumpedStr, changed: true }; 132 } 133 } else if (tnn < fnn) { 134 try { if (toInp) toInp.value = ''; } catch (_e3c) {} 135 setMirrorToInputs(''); 136 return { from: String(f || ''), to: '', changed: true }; 137 } 138 return { from: String(f || ''), to: String(t || ''), changed: false }; 139 }; 140 141 legacy.ribbonFilterOnOk = function (opts) { 142 try { 143 var fn = getFromShared('rangeOnOk'); 144 if (fn) return fn(opts || {}); 145 } catch (_e) {} 146 opts = opts || {}; 147 var fromInp = opts.fromInp, toInp = opts.toInp, sanitize = opts.sanitize; 148 var storedFrom = opts.storedFrom, storedTo = opts.storedTo; 149 var syncBeforeSubmit = opts.syncBeforeSubmit, submit = opts.submit, closeDd = opts.closeDd; 150 var curFrom = String((sanitize && sanitize(fromInp && fromInp.value)) || ''); 151 var curTo = String((sanitize && sanitize(toInp && toInp.value)) || ''); 152 if (curFrom === storedFrom && curTo === storedTo) { if (closeDd) closeDd(); return false; } 153 if (typeof syncBeforeSubmit === 'function') syncBeforeSubmit(); 154 if (typeof submit === 'function') submit(); 155 if (closeDd) closeDd();
156 return true; 157 }; 158 159 legacy.canAjaxList = function () { 160 try { 161 var fn = getFromShared('canAjaxList'); 162 if (fn) return !!fn(); 163 } catch (_e) {} 164 try { if (window.__disableAjaxList) return false; } catch (_e2) {} 165 return !!(legacy.q('.wrap-left') && typeof window.loadOffers === 'function'); 166 }; 167 168 legacy.persistFilterState = function () { 169 try { 170 var fn = getFromShared('persistFilterState'); 171 if (fn) { fn(); return; } 172 } catch (_e) {} 173 try { 174 var persist = window.__mirelaPersistLastListContext || window.__mirelaPersistListFiltersState; 175 if (typeof persist === 'function') persist(); 176 } catch (_e3) {} 177 }; 178 179 legacy.refreshAdvancedCounter = function (delayMs) { 180 try { 181 var fn = getFromShared('refreshCounterDelayed'); 182 if (fn) { fn(delayMs); return; } 183 } catch (_e) {} 184 var ms = (typeof delayMs === 'number') ? delayMs : 50; 185 setTimeout(function () { 186 try { if (typeof window.updateAdvancedFiltersCount === 'function') window.updateAdvancedFiltersCount(); } catch (_e2) {} 187 }, ms); 188 }; 189 190 legacy.submitCurrentSearch = function (opts) { 191 try { 192 var fn = getFromShared('submitCurrentSearch'); 193 if (fn) return !!fn(opts || {}); 194 } catch (_e) {} 195 opts = opts || {}; 196 var form = opts.form || legacy.q('.js-search-form'); 197 if (!form) return false; 198 try { window.__ribbonOkSubmit = true; } catch (_e2) {} 199 try { 200 var requestPolicy = window.MirelaSearch && window.MirelaSearch.requestPolicy; 201 if (requestPolicy && typeof requestPolicy.runSearchAction === 'function') { 202 return !!requestPolicy.runSearchAction('search_button', { form: form, force: true }); 203 } 204 if (typeof form.requestSubmit === 'function') form.requestSubmit(); 205 else form.submit(); 206 } finally { 207 try { window.__ribbonOkSubmit = false; } catch (_e3) {} 208 } 209 return true; 210 }; 211 212 /** 213 * Home / shared search bar: must use jQuery submit event with isProgrammatic set. 214 * Native form.dispatchEvent(new Event('submit')) often does not surface isProgrammatic to 215 * $('.js-search-form').on('submit', â¦) the way home.js expects â falls through to buildHomeListUrl. 216 */ 217 legacy.dispatchProgrammaticSearchSubmit = function (form) { 218 form = form || legacy.q('.js-search-form'); 219 if (!form) return; 220 var usedJq = false; 221 try { window.__ribbonOkSubmit = true; } catch (_e0) {} 222 try { 223 var $ = window.jQuery || window.$; 224 if ($ && $.fn && typeof $.fn.trigger === 'function') { 225 var jqEv = $.Event('submit', { cancelable: true, bubbles: true }); 226 jqEv.isProgrammatic = true; 227 $(form).trigger(jqEv); 228 usedJq = true; 229 } 230 } catch (_e1) {} 231 if (!usedJq) { 232 try { 233 var ev = new Event('submit', { cancelable: true, bubbles: true }); 234 ev.isProgrammatic = true; 235 form.dispatchEvent(ev); 236 } catch (_e2) {} 237 } 238 try { window.__ribbonOkSubmit = false; } catch (_e3) {} 239 }; 240 241 legacy.rangeCloseOpposite = function (opts) { 242 try { var fn = getFromShared('rangeCloseOpposite'); if (fn) return fn(opts || {}); } catch (_e) {} 243 }; 244 legacy.queueRangeSubmit = function (form) { 245 try { var fn = getFromShared('rangeQueueSubmit'); if (fn) return fn(form); } catch (_e) {} 246 }; 247 legacy.handleRangeClear = function (opts) { 248 try { var fn = getFromShared('rangeHandleClear'); if (fn) return fn(opts || {}); } catch (_e) {} 249 }; 250 legacy.bindRangeInputShield = function (fromInp, toInp) { 251 try { var fn = getFromShared('bindRangeInputShield'); if (fn) return fn(fromInp, toInp); } catch (_e) {} 252 }; 253 legacy.prefillRangeFromSearchBar = function (opts) { 254 try { var fn = getFromShared('prefillRangeFromSearchBar'); if (fn) return fn(opts || {}); } catch (_e) {} 255 }; 256 legacy.bindAdvancedRangeMirrors = function (opts) { 257 try { var fn = getFromShared('bindAdvancedRangeMirrors'); if (fn) return fn(opts || {}); } catch (_e) {} 258 }; 259 legacy.bindRangeSubmitSync = function (opts) { 260 try { var fn = getFromShared('bindRangeSubmitSync'); if (fn) return fn(opts || {}); } catch (_e) {} 261 }; 262 legacy.bindRangeOutsideClose = function (opts) { 263 try { var fn = getFromShared('bindRangeOutsideClose'); if (fn) return fn(opts || {}); } catch (_e) {} 264 return function () {}; 265 }; 266 legacy.bindRangeBlurCommit = function (opts) { 267 try { var fn = getFromShared('bindRangeBlurCommit'); if (fn) return fn(opts || {}); } catch (_e) {} 268 }; 269 legacy.createRangeListAdapter = function (opts) { 270 try { var fn = getFromShared('createRangeListAdapter'); if (fn) return fn(opts || {}); } catch (_e) {} 271 return null; 272 }; 273 274 // Backward-compatible globals (delegating to namespaced legacy bridge). 275 window.__mirelaGetSharedApi = legacy.getSharedApi; 276 window.__mirelaQ = legacy.q; 277 window.__mirelaRibbonFilterCapture = legacy.ribbonFilterCapture; 278 window.__mirelaNormalizeRibbonRange = legacy.normalizeRibbonRange; 279 window.__mirelaRibbonFilterOnOk = legacy.ribbonFilterOnOk; 280 window.__mirelaCanAjaxList = legacy.canAjaxList; 281 window.__mirelaPersistFilterState = legacy.persistFilterState; 282 window.__mirelaRefreshAdvancedCounter = legacy.refreshAdvancedCounter; 283 window.__mirelaSubmitCurrentSearch = legacy.submitCurrentSearch; 284 window.__mirelaDispatchProgrammaticSearchSubmit = legacy.dispatchProgrammaticSearchSubmit; 285 window.__mirelaRangeCloseOpposite = legacy.rangeCloseOpposite; 286 window.__mirelaQueueRangeSubmit = legacy.queueRangeSubmit; 287 window.__mirelaHandleRangeClear = legacy.handleRangeClear; 288 window.__mirelaBindRangeInputShield = legacy.bindRangeInputShield; 289 window.__mirelaPrefillRangeFromSearchBar = legacy.prefillRangeFromSearchBar; 290 window.__mirelaBindAdvancedRangeMirrors = legacy.bindAdvancedRangeMirrors; 291 window.__mirelaBindRangeSubmitSync = legacy.bindRangeSubmitSync; 292 window.__mirelaBindRangeOutsideClose = legacy.bindRangeOutsideClose; 293 window.__mirelaBindRangeBlurCommit = legacy.bindRangeBlurCommit; 294 window.__mirelaCreateRangeListAdapter = legacy.createRangeListAdapter; 295})(window, document); 296 297 298// ---- END: js/components/mirela-search-legacy-bridge.js ---- 299 300// ---- START: js/components/search-route-utils.js ---- 301/* Shared route/deal/locale helpers for search flows. */ 302(function (window) { 303 'use strict'; 304 305 var root = window.MirelaSearch = window.MirelaSearch || {}; 306 var api = root.routeUtils = root.routeUtils || {}; 307 308 var DEAL_SEGMENT_RE = '(?:prodazhbi|naemi|for-sale|for-rent|prodazha|arenda|verkauf|miete|kupuvachi|naemateli)'; 309 var DEAL_SEGMENT_EXACT_RE = new RegExp('^' + DEAL_SEGMENT_RE + '$', 'i'); 310 var LIST_ROUTE_RE = new RegExp('^/(?:en|de|ru)?/?' + DEAL_SEGMENT_RE + '(?:/|$)', 'i');
311 var OFFER_LIKE_PATH_RE = new RegExp('/' + DEAL_SEGMENT_RE + '/[^/]+/[^/]+-\\d+(?:\\.html)?$', 'i'); 312 var DEAL_SEGMENT_PATH_RE = new RegExp('/' + DEAL_SEGMENT_RE + '/', 'i'); 313 314 function getPathSegments(pathname) { 315 return String(pathname || '').split('/').filter(Boolean); 316 } 317 318 function isLocaleSegment(seg) { 319 var s = String(seg || '').toLowerCase(); 320 return (s === 'bg' || s === 'en' || s === 'ru' || s === 'de'); 321 } 322 323 function detectLocaleFromPath(pathname) { 324 try { 325 var first = getPathSegments(pathname || '')[0] || ''; 326 return isLocaleSegment(first) ? String(first).toLowerCase() : 'bg'; 327 } catch (_e) { 328 return 'bg'; 329 } 330 } 331 332 function getLocalePrefix(pathname, opts) { 333 try { 334 var o = opts || {}; 335 var includeBg = !!o.includeBg; 336 var locale = detectLocaleFromPath(pathname || window.location.pathname || ''); 337 if (locale === 'bg' && !includeBg) return ''; 338 return '/' + locale; 339 } catch (_e) { 340 return ''; 341 } 342 } 343 344 function isDealSegment(seg) { 345 return DEAL_SEGMENT_EXACT_RE.test(String(seg || '')); 346 } 347 348 function isListRoutePath(pathname) { 349 try { 350 var p = String(pathname || '').toLowerCase(); 351 return LIST_ROUTE_RE.test(p); 352 } catch (_e) { 353 return false; 354 } 355 } 356 357 function isOfferLikePath(pathname) { 358 try { 359 var p = String(pathname || '').trim(); 360 if (!p) return false; 361 if (/\/offer\/\d+(?:\/)?$/i.test(p)) return true; 362 return OFFER_LIKE_PATH_RE.test(p); 363 } catch (_e) { 364 return false; 365 } 366 } 367 368 function extractOfferIdFromPath(pathname) { 369 try { 370 var path = String(pathname || '').trim(); 371 if (!path) return ''; 372 var offerPathMatch = path.match(/\/offer\/(\d+)(?:\/)?$/i); 373 if (offerPathMatch) return String(offerPathMatch[1] || '').trim(); 374 if (!isOfferLikePath(path)) return ''; 375 var slugPathMatch = path.match(/-(\d+)(?:\.html)?$/i); 376 return slugPathMatch ? String(slugPathMatch[1] || '').trim() : ''; 377 } catch (_e) { 378 return ''; 379 } 380 } 381 382 function buildOfferRedirectUrl(offerId, pathname) { 383 var id = String(offerId || '').trim(); 384 var safeId = encodeURIComponent(id); 385 if (!safeId) return '/offer/'; 386 return getLocalePrefix(pathname || window.location.pathname || '') + '/offer/' + safeId; 387 } 388 389 function getDealSegmentIndex(pathParts) { 390 var parts = Array.isArray(pathParts) ? pathParts : []; 391 if (!parts.length) return 0; 392 return isLocaleSegment(parts[0]) ? 1 : 0; 393 } 394 395 function containsDealSegmentPath(hrefOrPath) { 396 return DEAL_SEGMENT_PATH_RE.test(String(hrefOrPath || '')); 397 } 398 399 /** 400 * Preset accent list URLs: /{deal}/{slug}-offer{ID} â filters may exist only server-side. 401 * Used by URL authority / hydrator to avoid stripping SSR preset facets. 402 */ 403 function isPresetAccentListPath(pathname) { 404 try { 405 var path = String(pathname || '').trim(); 406 return /^\/(?:en\/|ru\/|de\/)?(?:prodazhbi|naemi|for-sale|for-rent|prodazha|arenda|verkauf|miete)\/[^/]+-offer\d+(?:\.html)?$/i.test(path); 407 } catch (_e) { 408 return false; 409 } 410 } 411 412 /** True when path is only locale (optional) + deal segment â no city/preset slug after the deal. */ 413 function isCollapsedDealOnlyListPath(pathname) { 414 var parts = getPathSegments(pathname); 415 if (!parts.length) return false; 416 var i = getDealSegmentIndex(parts); 417 if (!isDealSegment(parts[i] || '')) return false; 418 return parts.length <= i + 1; 419 } 420 421 /** 422 * When replaceState collapses /{deal}/{slug}-offer{id} to /{deal}?page=â¦, restore the accent pathname 423 * so __preset_id and URL sync stay aligned with the pinned preset list session. 424 */ 425 function resolvePresetAccentPathnameForList(pathname) { 426 var path = String(pathname != null ? pathname : '').trim(); 427 if (!path && typeof window !== 'undefined') { 428 path = String(window.location.pathname || ''); 429 } 430 if (isPresetAccentListPath(path)) return path; 431 var pinned = ''; 432 if (typeof window !== 'undefined' && window.__mirelaPinnedPresetListPathname) { 433 pinned = String(window.__mirelaPinnedPresetListPathname || '').trim(); 434 } 435 if (!pinned || !isPresetAccentListPath(pinned)) return path; 436 if (!isCollapsedDealOnlyListPath(path)) return path; 437 return pinned; 438 } 439 440 /** Clear pinned preset when user navigates to a non-accent list path (e.g. /{deal}/grad-â¦). */ 441 function shouldClearPresetAccentPin(pathname) { 442 var parts = getPathSegments(pathname);
443 var i = getDealSegmentIndex(parts); 444 if (parts.length <= i + 1) return false; 445 var seg = String(parts[i + 1] || ''); 446 if (/-offer\d/i.test(seg)) return false; 447 return true; 448 } 449 450 api.getPathSegments = getPathSegments; 451 api.isLocaleSegment = isLocaleSegment; 452 api.detectLocaleFromPath = detectLocaleFromPath; 453 api.getLocalePrefix = getLocalePrefix; 454 api.isDealSegment = isDealSegment; 455 api.isListRoutePath = isListRoutePath; 456 api.isOfferLikePath = isOfferLikePath; 457 api.extractOfferIdFromPath = extractOfferIdFromPath; 458 api.buildOfferRedirectUrl = buildOfferRedirectUrl; 459 api.getDealSegmentIndex = getDealSegmentIndex; 460 api.containsDealSegmentPath = containsDealSegmentPath; 461 api.isPresetAccentListPath = isPresetAccentListPath; 462 api.isCollapsedDealOnlyListPath = isCollapsedDealOnlyListPath; 463 api.resolvePresetAccentPathnameForList = resolvePresetAccentPathnameForList; 464 api.shouldClearPresetAccentPin = shouldClearPresetAccentPin; 465})(window); 466 467// ---- END: js/components/search-route-utils.js ---- 468 469// ---- START: js/components/search-canonical-utils.js ---- 470/* Shared canonical URL cleanup helpers for list/search pages. */ 471(function (window) { 472 'use strict'; 473 474 var root = window.MirelaSearch = window.MirelaSearch || {}; 475 var api = root.canonicalUtils = root.canonicalUtils || {}; 476 477 function getRouteUtils() { 478 try { 479 return (window.MirelaSearch && window.MirelaSearch.routeUtils) ? window.MirelaSearch.routeUtils : null; 480 } catch (_e) { 481 return null; 482 } 483 } 484 485 function getPathSegments(pathLike) { 486 var raw = String(pathLike || '').trim(); 487 if (!raw) return []; 488 try { 489 var u = new URL(raw, window.location.origin); 490 raw = String(u.pathname || ''); 491 } catch (_eUrl) { 492 raw = raw.split('#')[0].split('?')[0]; 493 } 494 return raw.replace(/^\/+/, '').split('/').filter(Boolean); 495 } 496 497 function isLocaleSegment(seg, opts) { 498 try { 499 if (opts && typeof opts.isLocaleSegment === 'function') { 500 return !!opts.isLocaleSegment(seg); 501 } 502 } catch (_e0) {} 503 try { 504 var route = getRouteUtils(); 505 if (route && typeof route.isLocaleSegment === 'function') { 506 return !!route.isLocaleSegment(seg); 507 } 508 } catch (_e1) {} 509 var s = String(seg || '').toLowerCase(); 510 return (s === 'bg' || s === 'en' || s === 'ru' || s === 'de'); 511 } 512 513 function toUniqueValues(values) { 514 var out = []; 515 var seen = {}; 516 (values || []).forEach(function (v) { 517 var s = String(v || '').trim(); 518 if (!s) return; 519 if (s.indexOf(',') !== -1) { 520 s.split(',').forEach(function (x) { 521 var sx = String(x || '').trim(); 522 if (!sx || seen[sx]) return; 523 seen[sx] = true; 524 out.push(sx); 525 }); 526 return; 527 } 528 if (seen[s]) return; 529 seen[s] = true; 530 out.push(s); 531 }); 532 return out; 533 } 534 535 function isLegacyStreetPathSegment(seg) { 536 return /^street-\d+$/i.test(String(seg || '').trim()); 537 } 538 539 function isStreetSeoPrefixSegment(seg) { 540 var s = String(seg || '').trim().toLowerCase(); 541 return s === 'street' || s === 'ulitsa' || s === 'strasse' 542 || s === 'kompleks' || s === 'complex' || s === 'komplex'; 543 } 544 545 function isStreetSeoSlugSegment(seg) { 546 return /.+-\d+$/i.test(String(seg || '').trim()); 547 } 548 549 function cleanupPathEncodedFilters(params, opts) { 550 var usp = (params instanceof URLSearchParams) ? params : new URLSearchParams(); 551 var o = opts || {}; 552 try { 553 var bp = String((o.basePath == null ? '' : o.basePath) || '').trim(); 554 if (!bp) bp = String(window.location.pathname || ''); 555 556 var pathParts = getPathSegments(bp); 557 var firstSeg = String(pathParts[0] || '').toLowerCase(); 558 var hasLocale = isLocaleSegment(firstSeg, o); 559 var cityIdx = hasLocale ? 2 : 1; 560 var segAfterCity = String(pathParts[cityIdx + 1] || '').trim(); 561 var segAfterDistrict = String(pathParts[cityIdx + 2] || '').trim(); 562 563 if (pathParts[cityIdx]) { 564 usp.delete('city_id'); 565 usp.delete('city_id[]'); 566 } 567 568 var isLegacyStreetPathSeg = isLegacyStreetPathSegment(segAfterCity); 569 var isNamespacedStreetPath = isStreetSeoPrefixSegment(segAfterCity) && isStreetSeoSlugSegment(segAfterDistrict); 570 var isStreetPathSeg = isLegacyStreetPathSeg || isNamespacedStreetPath; 571 var districtVals = [] 572 .concat(usp.getAll('district_id[]') || []) 573 .concat(usp.getAll('district_id') || []); 574 var districtUniq = toUniqueValues(districtVals); 575 var hasDistrictGroups = usp.has('district_group_id') || usp.has('district_group_id[]'); 576 var hasStreetFilter = usp.has('street_id'); 577 var singleDistrictCore = districtUniq.length === 1 && !hasDistrictGroups && !hasStreetFilter && !isStreetPathSeg; 578 var hasPreciseLocationFilter = districtUniq.length > 0 || hasDistrictGroups || hasStreetFilter; 579 var hasDistrictInPath = !isStreetPathSeg && (!!segAfterDistrict || (!!segAfterCity && hasPreciseLocationFilter)); 580 if (hasDistrictInPath) { 581 usp.delete('district_id'); 582 usp.delete('district_id[]'); 583 usp.delete('district_group_id'); 584 usp.delete('district_group_id[]'); 585 } 586 587 if (isStreetPathSeg) { 588 try { usp.delete('street_id'); } catch (_eSt) {} 589 } 590 591 var etypeIdx = isNamespacedStreetPath ? -1 : (segAfterDistrict ? (cityIdx + 2)
591: (cityIdx + 1)); 592 if (etypeIdx >= 0 && pathParts[etypeIdx] && !isLegacyStreetPathSegment(pathParts[etypeIdx])) { 593 var etypeSlugInPath = String(pathParts[etypeIdx] || '').trim().toLowerCase(); 594 var selectedPt = [] 595 .concat(usp.getAll('property_type[]') || []) 596 .concat(usp.getAll('property_type') || []); 597 selectedPt = toUniqueValues(selectedPt); 598 599 var isExactGroupEncoded = false; 600 try { 601 var cfg = o.topTypesConfig || ((typeof window !== 'undefined' && window.__mirelaTopTypesConfig) ? window.__mirelaTopTypesConfig : {}); 602 var groups = Array.isArray(cfg.groups) ? cfg.groups : []; 603 var selectedSorted = selectedPt.slice().sort(); 604 groups.forEach(function (g) { 605 if (!g) return; 606 var gSlug = String(g.slug || '').trim().toLowerCase(); 607 if (!gSlug || gSlug !== etypeSlugInPath) return; 608 var ids = (Array.isArray(g.type_ids) ? g.type_ids : []) 609 .map(function (v) { return String(v || '').trim(); }) 610 .filter(Boolean) 611 .sort(); 612 if (ids.length && ids.length === selectedSorted.length && ids.every(function (v, i) { return v === selectedSorted[i]; })) { 613 isExactGroupEncoded = true; 614 } 615 }); 616 } catch (_eCfg) {} 617 618 var isSingleTypeEncoded = selectedPt.length === 1; 619 if (isExactGroupEncoded || isSingleTypeEncoded) { 620 usp.delete('property_type'); 621 usp.delete('property_type[]'); 622 } 623 } 624 } catch (_e) {} 625 626 return usp; 627 } 628 629 /** 630 * When the list pathname already encodes location beyond city (district slug, street-*, etype, â¦), 631 * drop non-numeric ?s= â it duplicates the visible SEO path and hurts canonical URLs. 632 * Pure numeric ?s= is kept (offer-id search). 633 */ 634 function dropRedundantLocationSParam(params, opts) { 635 var usp = (params instanceof URLSearchParams) ? params : new URLSearchParams(); 636 var o = opts || {}; 637 try { 638 var sVal = String(usp.get('s') || '').trim(); 639 if (sVal === '' || /^\d+$/.test(sVal)) { 640 return usp; 641 } 642 643 var bp = String((o.basePath == null ? '' : o.basePath) || '').trim(); 644 if (!bp) { 645 bp = String(typeof window !== 'undefined' ? (window.location.pathname || '') : ''); 646 } 647 648 var pathParts = getPathSegments(bp); 649 var firstSeg = String(pathParts[0] || '').toLowerCase(); 650 var hasLocale = isLocaleSegment(firstSeg, o); 651 var cityIdx = hasLocale ? 2 : 1; 652 var segAfterCity = String(pathParts[cityIdx + 1] || '').trim(); 653 654 if (!segAfterCity) { 655 return usp; 656 } 657 658 usp.delete('s'); 659 } catch (_e) {} 660 661 return usp; 662 } 663 664 function writeBrowseContractCanonicalFromServer(rawUrl) { 665 try { 666 if (typeof window === 'undefined') { 667 return; 668 } 669 var s = String(rawUrl || '').trim(); 670 if (!s) { 671 return; 672 } 673 var u = new URL(s, window.location.origin); 674 var rel = (u.pathname || '') + (u.search || '') + (u.hash || ''); 675 if (!rel || rel.charAt(0) !== '/') { 676 return; 677 } 678 window.__mirelaBrowseContractCanonicalRel = rel; 679 window.__mirelaBrowseContractCanonicalAt = Date.now(); 680 window.__mirelaLastMapBrowseSeoHref = rel; 681 } catch (_eW) {} 682 } 683 684 /** 685 * @return string Relative path+query+hash (/prodazhbi?â¦); empty if none. 686 */ 687 function readBrowseContractCanonicalRel() { 688 try { 689 var pri = String( 690 typeof window !== 'undefined' ? (window.__mirelaBrowseContractCanonicalRel || '') : '' 691 ).trim(); 692 if (pri) { 693 var u1 = new URL(pri, window.location.origin); 694 return (u1.pathname || '') + (u1.search || '') + (u1.hash || ''); 695 } 696 } catch (_e1) {} 697 try { 698 var fb = String( 699 typeof window !== 'undefined' ? (window.__mirelaLastMapBrowseSeoHref || '') : '' 700 ).trim(); 701 if (fb) { 702 var u2 = new URL(fb, window.location.origin); 703 return (u2.pathname || '') + (u2.search || '') + (u2.hash || ''); 704 } 705 } catch (_e2) {} 706 return ''; 707 } 708 709 /** First path segment after optional locale â used to avoid crossâdeal-type stale canonicals. */ 710 function listingDealSlugKey(pathname) { 711 var parts = getPathSegments(pathname || ''); 712 if (!parts.length) { 713 return ''; 714 } 715 if (isLocaleSegment(parts[0])) { 716 parts.shift(); 717 } 718 719 return String(parts[0] || '').toLowerCase(); 720 } 721 722 api.writeBrowseContractCanonicalFromServer = writeBrowseContractCanonicalFromServer; 723 api.readBrowseContractCanonicalRel = readBrowseContractCanonicalRel; 724 api.listingDealSlugKey = listingDealSlugKey; 725 726 api.cleanupPathEncodedFilters = cleanupPathEncodedFilters; 727 api.dropRedundantLocationSParam = dropRedundantLocationSParam; 728})(window); 729 730// ---- END: js/components/search-canonical-utils.js ---- 731 732// ---- START: js/components/search-form-query-utils.js ---- 733/* Shared helpers for turning serialized search forms into query strings. */ 734(function (window) { 735 'use strict'; 736 737 var root = window.MirelaSearch = window.MirelaSearch || {}; 738 var api = root.formQueryUtils = root.formQueryUtils || {}; 739 740 function buildQueryString(formData, opts) { 741 var options = opts || {}; 742 var remove = new Set(Array.isArray(options.remove) ? options.remove.map(String) : []); 743 var override = (options.override && typeof options.override === 'object') ? options.override : {}; 744 var pairs = (Array.isArray(formData) ? formData : []) 745 .filter(function (item) { return item && item.name && item.name !== '_token'; }) 746 .filter(function (item) { return item.value !== undefined && item.value !== null && String(item.value).trim() !== ''; }) 747 .filter(function (item) { return !remove.has(String(item.name)); }) 748 .filter(function (item) { return !Object.prototype.hasOwnProperty.call(override, String(item.name)); }); 749 750 Object.keys(override).forEach(function (key) { 751 var value = override[key]; 752 if (value === undefined || value === null || String(value).trim() === '') return; 753 pairs.push({ name: key, value: String(value) }); 754 }); 755 756 return pairs 757 .map(function (item) { return encodeURIComponent(item.name) + '=' + encodeURIComponent(item.value); }) 758 .join('&'); 759 } 760 761 function serializeJqueryForm($form, opts) { 762 try { 763 var pairs = serializeJqueryFormArray($form); 764 return buildQueryString(pairs, opts); 765 } catch (_e) { 766 return ''; 767 } 768 } 769 770 /** Raw jQuery serializeArray (e.g. list offers pipeline); same filtering rules as consumers apply later. */ 771 function serializeJqueryFormArray($form) { 772 try { 773 return ($form && $form.serializeArray) ? $form.serializeArray() : []; 774 } catch (_e) { 775 return []; 776 } 777 } 778 779 api.buildQueryString = buildQueryString;
780 api.serializeJqueryForm = serializeJqueryForm; 781 api.serializeJqueryFormArray = serializeJqueryFormArray; 782})(window); 783 784// ---- END: js/components/search-form-query-utils.js ---- 785 786// ---- START: js/components/search-offer-type-filter-sync.js ---- 787/** 788 * Shared: clearing price/area filter fields and syncing map bounds into .js-location-map 789 * when switching offer type (home tabs / list deal-type). UI shells differ; DOM targets align. 790 */ 791(function (window, document) { 792 'use strict'; 793 794 var root = (window.MirelaSearch = window.MirelaSearch || {}); 795 796 function resolveForm(getSearchFormElement) { 797 if (typeof getSearchFormElement === 'function') { 798 try { 799 var f = getSearchFormElement(); 800 if (f) return f; 801 } catch (_e) {} 802 } 803 try { 804 return document.querySelector('.js-search-form'); 805 } catch (_e2) { 806 return null; 807 } 808 } 809 810 function patchSearchFilters(patch, reason) { 811 void reason; 812 try { 813 var target = (typeof window.filterState === 'object' && window.filterState) ? window.filterState : null; 814 if (!target) return false; 815 Object.keys(patch || {}).forEach(function (key) { 816 target[key] = String((patch || {})[key] == null ? '' : (patch || {})[key]).trim(); 817 }); 818 return true; 819 } catch (_eState) { 820 return false; 821 } 822 } 823 824 function clearSearchBarPriceAreaStorage() { 825 patchSearchFilters( 826 { price_from: '', price_to: '', area_from: '', area_to: '' }, 827 'search-offer-type-filter-sync.clearSearchBarPriceAreaStorage' 828 ); 829 } 830 831 function clearVisiblePriceInputs() { 832 try { 833 Array.prototype.slice.call(document.querySelectorAll( 834 '.wd-search-form input[name="price_from"], ' + 835 '.wd-search-form input[name="price_to"], ' + 836 '.wd-search-form input[name="price_from_eur"], ' + 837 '.wd-search-form input[name="price_to_eur"], ' + 838 '.wd-search-form .js-mirela-range-from, ' + 839 '.wd-search-form .js-mirela-range-to' 840 )).forEach(function (field) { 841 if (!field) return; 842 try { 843 field.value = ''; 844 } catch (_eValue) {} 845 try { 846 field.dispatchEvent(new Event('input', { bubbles: true })); 847 } catch (_eInput) {} 848 try { 849 field.dispatchEvent(new Event('change', { bubbles: true })); 850 } catch (_eChange) {} 851 }); 852 } catch (_ePriceUi) {} 853 } 854 855 function clearTopRibbonPriceUi() { 856 try { 857 Array.prototype.slice.call(document.querySelectorAll('.js-top-price-dd')).forEach(function (dd) { 858 if (!dd) return; 859 try { 860 dd.classList.remove('has-selection'); 861 dd.classList.remove('open'); 862 } catch (_eDdCls) {} 863 try { 864 var wrap = dd.closest('.top-mini-dd'); 865 if (wrap) wrap.classList.remove('open'); 866 } catch (_eWrap) {} 867 try { 868 if (dd.dataset) { 869 dd.dataset.mirelaRangeOpenFrom = ''; 870 dd.dataset.mirelaRangeOpenTo = ''; 871 } 872 dd.setAttribute('data-mirela-range-open-from', ''); 873 dd.setAttribute('data-mirela-range-open-to', ''); 874 } catch (_eData) {} 875 try { 876 var current = dd.querySelector('.current'); 877 var def = String((dd.getAttribute('data-default-label') || '').trim()); 878 if (current) current.textContent = def || 'Цена'; 879 } catch (_eCurrent) {} 880 try { 881 dd.querySelectorAll('.js-mirela-range-from, .js-mirela-range-to').forEach(function (field) { 882 try { field.value = ''; } catch (_eFieldValue) {} 883 }); 884 } catch (_eFields) {} 885 }); 886 } catch (_ePriceRibbon) {} 887 } 888 889 function normalizeDealType(typeValue) { 890 var normalized = String(typeValue == null ? '' : typeValue).trim(); 891 return normalized === '1' || normalized === '2' || normalized === '3' || normalized === '4' ? normalized : ''; 892 } 893 894 function shouldClearPriceOnDealTypeSwitch(previousType, nextType) { 895 var prev = normalizeDealType(previousType); 896 var next = normalizeDealType(nextType); 897 if (!prev || !next || prev === next) return false; 898 if ((prev === '1' && next === '3') || (prev === '2' && next === '4')) return false; 899 if ((prev === '4' && next === '2') || (prev === '3' && next === '1')) return false; 900 return true; 901 } 902 903 /** List + any page: centralized ribbon (+ filterState price), SearchBar slice, optional URL strip. */ 904 function clearPriceFieldsAndUrl(deps) { 905 var d = deps || {}; 906 if (d.skipWhenDealTypePairDisallows === true && !shouldClearPriceOnDealTypeSwitch(d.previousType, d.nextType)) { 907 return false; 908 } 909 var form = resolveForm(d.getSearchFormElement); 910 try { 911 if (form) { 912 var hFrom = form.querySelector('input[name="price_from"]'); 913 var hTo = form.querySelector('input[name="price_to"]'); 914 if (hFrom) hFrom.value = ''; 915 if (hTo) hTo.value = ''; 916 } 917 try { 918 if (typeof window.clearRibbonPriceFilterState === 'function') { 919 window.clearRibbonPriceFilterState(); 920 } 921 } catch (_eFs) {} 922 patchSearchFilters({ price_from: '', price_to: '' }, 'search-offer-type-filter-sync.clearPriceFieldsAndUrl'); 923 clearVisiblePriceInputs(); 924 clearTopRibbonPriceUi(); 925 try { 926 if (window.MirelaRangeFilters && typeof window.MirelaRangeFilters.refreshAll === 'function') { 927 window.MirelaRangeFilters.refreshAll(); 928 } 929 } catch (_ref) {} 930 if (typeof d.getCurrentSearchParams === 'function' && typeof d.replaceCurrentUrlFromParams === 'function') { 931 try { 932 var usp = d.getCurrentSearchParams(); 933 var changed = false; 934 if (usp && usp.has && usp.has('price_from')) { 935 usp.delete('price_from'); 936 changed = true; 937 } 938 if (usp && usp.has && usp.has('price_to')) { 939 usp.delete('price_to'); 940 changed = true; 941 } 942 if (changed) { 943 d.replaceCurrentUrlFromParams(usp, true); 944 } 945 } catch (_eUrl) {} 946 } 947 } catch (_e) {} 948 return true; 949 } 950 951 /** Home tabs 1â2: price + area fields + quick dropdown labels + SearchBar stored area. */ 952 function clearPriceAreaAndQuickLabelsForHome() { 953 try { 954 if (window.MirelaRangeFilters && typeof window.MirelaRangeFilters.clearAllRanges === 'function') { 955 window.MirelaRangeFilters.clearAllRanges(); 956 clearSearchBarPriceAreaStorage(); 957 return; 958 } 959 } catch (_eMr) {} 960 try { 961 var $ = window.jQuery; 962 if ($ && $.fn) { 963 $('.js-mirela-range-from').val(''); 964 $('.js-mirela-range-to').val(''); 965 var priceDd = $('.js-top-price-dd'); 966 if (priceDd.length) { 967 var defaultLabel = 968 priceDd.data('defaultLabel') || 969 (document.documentElement.lang === 'bg' ? 'Цена' : 'Price'); 970 priceDd.find('.current').text(defaultLabel); 971 priceDd.removeClass('has-selection'); 972 } 973 var areaDd = $('.js-top-area-dd'); 974 if (areaDd.length) { 975 var defaultArea = 976 areaDd.data('defaultLabel') || 977 (document.documentElement.lang === 'bg' ? 'ÐлоÑ' : 'Area'); 978 areaDd.find('.current').text(defaultArea); 979 areaDd.removeClass('has-selection'); 980 } 981 } 982 } catch (_eJq) {} 983 clearSearchBarPriceAreaStorage(); 984 } 985 986 /** 987 * If map hidden input is empty, write current Leaflet bounds pipe string (ne|sw|zoom). 988 * @param {HTMLInputElement|null|undefined} mapInputEl 989 * @param {*} MapLeaflet â module with getMap() 990 */ 991 function fillMapBoundsFromLeafletIfEmpty(mapInputEl, MapLeaflet) { 992 try { 993 if (!mapInputEl || !MapLeaflet || typeof MapLeaflet.getMap !== 'function') return; 994 var v = String(mapInputEl.value || '').trim(); 995 if (v) return; 996 var m = MapLeaflet.getMap(); 997 if (!m || typeof m.getBounds !== 'function' || typeof m.getZoom !== 'function') return; 998 var b = m.getBounds(); 999 if (!b || typeof b.getNorthEast !== 'function' || typeof b.getSouthWest !== 'function') return; 1000 var ne = b.getNorthEast();
1001 var sw = b.getSouthWest(); 1002 if (!ne || !sw) return; 1003 mapInputEl.value = [ 1004 ne.lng.toFixed(5), 1005 ne.lat.toFixed(5), 1006 sw.lng.toFixed(5), 1007 sw.lat.toFixed(5), 1008 m.getZoom() 1009 ].join('|'); 1010 } catch (_e) {} 1011 } 1012 1013 root.searchOfferTypeFilterSync = { 1014 shouldClearPriceOnDealTypeSwitch: shouldClearPriceOnDealTypeSwitch, 1015 clearPriceFieldsAndUrl: clearPriceFieldsAndUrl, 1016 clearPriceAreaAndQuickLabelsForHome: clearPriceAreaAndQuickLabelsForHome, 1017 fillMapBoundsFromLeafletIfEmpty: fillMapBoundsFromLeafletIfEmpty 1018 }; 1019})(window, document); 1020 1021// ---- END: js/components/search-offer-type-filter-sync.js ---- 1022 1023// ---- START: js/components/location/location-selection-request.js ---- 1024/* Pure helpers for location selection request payloads. */ 1025(function (window) { 1026 'use strict'; 1027 1028 var api = window.MirelaLocationSelectionRequest = window.MirelaLocationSelectionRequest || {}; 1029 1030 function getNormalizedCityId(value) { 1031 try { 1032 if (!value) return ''; 1033 var cities = value.cities; 1034 return Array.isArray(cities) ? String(cities[0] || '').trim() : String(cities || '').trim(); 1035 } catch (_e) { 1036 return ''; 1037 } 1038 } 1039 1040 function toCsv(value) { 1041 try { 1042 if (Array.isArray(value)) { 1043 return value.filter(Boolean).map(function (v) { return String(v); }).join(','); 1044 } 1045 return value == null ? '' : String(value); 1046 } catch (_e) { 1047 return ''; 1048 } 1049 } 1050 1051 function hasMapBoundsValue(mapValue) { 1052 try { 1053 var mapStr = String(mapValue || '').trim(); 1054 return !!(mapStr && mapStr.split('|').length >= 4); 1055 } catch (_e) { 1056 return false; 1057 } 1058 } 1059 1060 /** 1061 * Build an AJAX-ready payload from a normalized or legacy location selection object. 1062 */ 1063 function buildSelectionRequestData(value, opts) { 1064 var options = opts || {}; 1065 var normalizedCityId = getNormalizedCityId(value); 1066 var mode = 'empty'; 1067 var ajaxData = {}; 1068 1069 try { 1070 if (value && value.districts) { 1071 mode = 'districts'; 1072 ajaxData.district_id = value.districts; 1073 if (normalizedCityId && options.includeCity !== false) ajaxData.city_id = normalizedCityId; 1074 } else if (value && value.district_groups) { 1075 mode = 'district_groups'; 1076 ajaxData.district_group_id = options.forceGroupArray && !Array.isArray(value.district_groups) 1077 ? [value.district_groups] 1078 : value.district_groups; 1079 if (normalizedCityId && options.includeCity !== false) ajaxData.city_id = normalizedCityId; 1080 } else if (value && value.street_id) { 1081 mode = 'street'; 1082 ajaxData.street_id = value.street_id; 1083 if (normalizedCityId && options.includeCity !== false) ajaxData.city_id = normalizedCityId; 1084 } else if (normalizedCityId) { 1085 mode = 'city'; 1086 if (options.includeCity !== false) ajaxData.city_id = normalizedCityId; 1087 if (options.forceCityOnce) { 1088 try { window.__forceCityIdOnce = String(normalizedCityId || ''); } catch (_eForce) {} 1089 } 1090 } 1091 } catch (_e) {} 1092 1093 return { 1094 cityId: normalizedCityId, 1095 mode: mode, 1096 ajaxData: ajaxData 1097 }; 1098 } 1099 1100 api.getNormalizedCityId = getNormalizedCityId; 1101 api.toCsv = toCsv; 1102 api.hasMapBoundsValue = hasMapBoundsValue; 1103 api.buildSelectionRequestData = buildSelectionRequestData; 1104})(window); 1105 1106// ---- END: js/components/location/location-selection-request.js ---- 1107 1108// ---- START: js/components/location/home-location-navigation-policy.js ---- 1109/* FilterState-first home location vs map navigation policy. */ 1110(function (window) { 1111 'use strict'; 1112 1113 var api = window.MirelaHomeLocationNavigationPolicy = window.MirelaHomeLocationNavigationPolicy || {}; 1114 1115 function readFilterStateSnapshot() { 1116 try { 1117 return (typeof window.filterState === 'object' && window.filterState) ? window.filterState : null; 1118 } catch (_e) { 1119 return null; 1120 } 1121 } 1122 1123 function readStringField(state, key) { 1124 try { 1125 if (!state || !Object.prototype.hasOwnProperty.call(state, key)) return ''; 1126 return String(state[key] || '').trim(); 1127 } catch (_e) { 1128 return ''; 1129 } 1130 } 1131 1132 function readArrayField(state, key) { 1133 try { 1134 var value = state && Object.prototype.hasOwnProperty.call(state, key) ? state[key] : null; 1135 if (!Array.isArray(value)) return []; 1136 return value.map(function (item) { return String(item || '').trim(); }
1136).filter(Boolean); 1137 } catch (_e) { 1138 return []; 1139 } 1140 } 1141 1142 function hasStrictLocationState(state, overrides) { 1143 var source = state || readFilterStateSnapshot() || {}; 1144 var options = overrides || {}; 1145 try { 1146 var streetId = Object.prototype.hasOwnProperty.call(options, 'streetId') 1147 ? String(options.streetId || '').trim() 1148 : readStringField(source, 'street_id'); 1149 if (streetId) return true; 1150 1151 var districtIds = Object.prototype.hasOwnProperty.call(options, 'districtIds') 1152 ? (Array.isArray(options.districtIds) ? options.districtIds : []) 1153 : readArrayField(source, 'district_id'); 1154 var groupIds = Object.prototype.hasOwnProperty.call(options, 'groupIds') 1155 ? (Array.isArray(options.groupIds) ? options.groupIds : []) 1156 : readArrayField(source, 'district_group_id'); 1157 1158 return districtIds.length > 0 || groupIds.length > 0;
1159 } catch (_e) { 1160 return false; 1161 } 1162 } 1163 1164 function shouldDropCityForBounds(opts) { 1165 var options = opts || {}; 1166 try { 1167 var hasMapBounds = !!options.hasMapBounds; 1168 var hasStrictLocation = !!options.hasStrictLocation; 1169 var searchTerm = String(options.searchTerm || '').trim(); 1170 var cityId = String(options.cityId || '').trim(); 1171 var defaultCityId = String(options.defaultCityId || '').trim(); 1172 if (!hasMapBounds || hasStrictLocation || searchTerm !== '') return false; 1173 return (!cityId || cityId === '0' || (defaultCityId && cityId === defaultCityId)); 1174 } catch (_e) { 1175 return false; 1176 } 1177 } 1178 1179 function clearHomeMapIntentFlags() { 1180 try { window.__mirelaHomeForceMapListNext = false; } catch (_e0) {} 1181 try { window.__mirelaHomeManualBoundsSearchDone = false; } catch (_e1) {} 1182 try { window.__mapManualNext = false; } catch (_e2) {} 1183 try { window.__manualSearchRequested = false; } catch (_e3) {} 1184 } 1185 1186 function setHomeListIntent(mode) { 1187 var normalized = ''; 1188 try { 1189 normalized = String(mode || '').trim().toLowerCase(); 1190 } catch (_eNorm) { 1191 normalized = ''; 1192 } 1193 if (normalized !== 'map' && normalized !== 'location') normalized = ''; 1194 1195 try { window.__mirelaHomeListIntent = normalized; } catch (_eWrite) {} 1196 1197 try { 1198 if (window.isHomePage !== true) return normalized; 1199 if (normalized === 'location') { 1200 clearHomeMapIntentFlags(); 1201 try { 1202 if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapHome === 'function') { 1203 window.MirelaSearch.applyUserMovedMapHome(false); 1204 } else { 1205 window.__mirelaUserMovedMapHome = false; 1206 } 1207 } catch (_eMoved) {} 1208 } else if (normalized === 'map') { 1209 try { window.__mirelaHomeCityScopedBrowseNext = false; } catch (_eCityScope) {} 1210 } 1211 } catch (_e) {} 1212 1213 return normalized; 1214 } 1215 1216 function getHomeDefaultCityId(opts) { 1217 var options = opts || {}; 1218 var defaultCityId = ''; 1219 try { 1220 defaultCityId = String(options.defaultCityId || '').trim(); 1221 } catch (_eOpt) { 1222 defaultCityId = ''; 1223 } 1224 if (defaultCityId) return defaultCityId; 1225 try { 1226 if (window.__mirelaHomeDefaultListCityId != null) { 1227 defaultCityId = String(window.__mirelaHomeDefaultListCityId || '').trim(); 1228 } 1229 } catch (_eGlobal) { 1230 defaultCityId = ''; 1231 } 1232 return defaultCityId; 1233 } 1234 1235 function paramObjLocksHomeListToMapViewport(paramObj) { 1236 var allowKeys = { 1237 type: true, 1238 deal: true, 1239 map: true, 1240 map_manual: true, 1241 map_bounds: true, 1242 map_user: true, 1243 map_lat: true, 1244 map_lng: true, 1245 map_zoom: true, 1246 page: true, 1247 city_id: true, 1248 'city_id[]': true, 1249 s: true, 1250 home_preload: true, 1251 _: true, 1252 _token: true, 1253 locale: true, 1254 focus_offer_id: true, 1255 restore_scroll: true 1256 }; 1257 try { 1258 if (!paramObj || typeof paramObj !== 'object') return false; 1259 var keys = Object.keys(paramObj); 1260 for (var i = 0; i < keys.length; i++) { 1261 var key = keys[i]; 1262 if (allowKeys[key]) continue; 1263 var value = paramObj[key]; 1264 if (value === null || value === undefined || value === '') continue; 1265 if (value === '0' || value === 0 || value === false) continue; 1266 if (Array.isArray(value) && value.length === 0) continue; 1267 return true; 1268 } 1269 } catch (_e) {} 1270 return false; 1271 } 1272 1273 /** 1274 * Decide whether home list navigation should use location navigation or map/bounds navigation. 1275 * Reads `filterState` first and accepts field overrides from callers. 1276 */ 1277 function getHomeListNavigationState(opts) { 1278 var options = opts || {}; 1279 var state = options.filterState || readFilterStateSnapshot() || {}; 1280 1281 var mapValue = Object.prototype.hasOwnProperty.call(options, 'mapValue') 1282 ? String(options.mapValue || '').trim() 1283 : readStringField(state, 'map'); 1284 var cityId = Object.prototype.hasOwnProperty.call(options, 'cityId') 1285 ? String(options.cityId || '').trim() 1286 : readStringField(state, 'city_id'); 1287 var searchTerm = Object.prototype.hasOwnProperty.call(options, 'searchTerm') 1288 ? String(options.searchTerm || '').trim() 1289 : readStringField(state, 's'); 1290 var intent = ''; 1291 1292 try { 1293 intent = String(window.__mirelaHomeListIntent || '').trim().toLowerCase(); 1294 } catch (_eIntent) { 1295 intent = ''; 1296 } 1297 1298 var hasStrictLocation = Object.prototype.hasOwnProperty.call(options, 'hasStrictLocation') 1299 ? !!options.hasStrictLocation 1300 : hasStrictLocationState(state, options); 1301 var hasMapBounds = Object.prototype.hasOwnProperty.call(options, 'hasMapBounds') 1302 ? !!options.hasMapBounds 1303 : api.hasMapBoundsValue(mapValue); 1304 var defaultCityId = getHomeDefaultCityId(options); 1305 var hasExplicitCity = !!( 1306 cityId && 1307 cityId !== '0' && 1308 (intent === 'location' || searchTerm !== '' || !defaultCityId || cityId !== defaultCityId) 1309 ); 1310 var hasExplicitLocation = !!(hasStrictLocation || hasExplicitCity); 1311 var hasFallbackMapIntent = false; 1312 var locksListToViewport = false; 1313 1314 try { 1315 hasFallbackMapIntent = !!( 1316 options.forceMapIntent === true || 1317 intent === 'map' || 1318 window.__mirelaHomeForceMapListNext === true || 1319 window.__mirelaUserMovedMapHome === true || 1320 window.__mirelaHomeManualBoundsSearchDone === true 1321 ); 1322 } catch (_eFallback) { 1323 hasFallbackMapIntent = (options.forceMapIntent === true || intent === 'map'); 1324 } 1325 1326 try { 1327 locksListToViewport = !!( 1328 hasMapBounds && 1329 !hasStrictLocation && 1330 options.paramObj && 1331 paramObjLocksHomeListToMapViewport(options.paramObj) 1332 ); 1333 } catch (_eLockViewport) { 1334 locksListToViewport = false; 1335 } 1336 1337 if (locksListToViewport) { 1338 hasExplicitCity = false; 1339 hasExplicitLocation = false; 1340 hasFallbackMapIntent = true; 1341 } 1342 1343 var shouldUseLocationNavigation = !!(hasExplicitLocation && intent !== 'map'); 1344 var shouldUseMapNavigation = !!(hasMapBounds && !hasExplicitLocation && hasFallbackMapIntent); 1345 1346 if (intent === 'location' && hasExplicitLocation) { 1347 shouldUseLocationNavigation = true; 1348 shouldUseMapNavigation = false; 1349 } 1350 if (options.forceMapIntent === true && hasMapBounds && !hasExplicitLocation) { 1351 shouldUseMapNavigation = true; 1352 shouldUseLocationNavigation = false; 1353 } 1354 if (hasStrictLocation) { 1355 shouldUseLocationNavigation = true; 1356 shouldUseMapNavigation = false; 1357 } 1358 1359 return { 1360 intent: intent, 1361 cityId: cityId, 1362 searchTerm: searchTerm, 1363 defaultCityId: defaultCityId, 1364 hasMapBounds: hasMapBounds, 1365 hasStrictLocation: hasStrictLocation, 1366 hasExplicitCity: hasExplicitCity, 1367 hasExplicitLocation: hasExplicitLocation, 1368 locksListToViewport: locksListToViewport, 1369 shouldUseLocationNavigation: shouldUseLocationNavigation, 1370 shouldUseMapNavigation: shouldUseMapNavigation 1371 }; 1372 } 1373 1374 api.readFilterStateSnapshot = readFilterStateSnapshot; 1375 api.getHomeDefaultCityId = getHomeDefaultCityId; 1376 api.hasStrictLocationState = hasStrictLocationState; 1377 api.hasMapBoundsValue = function (mapValue) { 1378 return !!(window.MirelaLocationSelectionRequest && typeof window.MirelaLocationSelectionRequest.hasMapBoundsValue === 'function' 1379 ? window.MirelaLocationSelectionRequest.hasMapBoundsValue(mapValue) 1380 : false); 1381 }; 1382 api.paramObjLocksHomeListToMapViewport = paramObjLocksHomeListToMapViewport; 1383 api.shouldDropCityForBounds = shouldDropCityForBounds; 1384 api.clearHomeMapIntentFlags = clearHomeMapIntentFlags; 1385 api.setHomeListIntent = setHomeListIntent; 1386 api.getHomeListNavigationState = getHomeListNavigationState; 1387})(window); 1388 1389// ---- END: js/components/location/home-location-navigation-policy.js ---- 1390 1391// ---- START: js/components/location/location-form-legacy-compat.js ---- 1392/* Legacy form compatibility helpers for location selection. */ 1393(function (window) { 1394 'use strict'; 1395 1396 var api = window.MirelaLocationFormLegacyCompat = window.MirelaLocationFormLegacyCompat || {}; 1397 var root = window.MirelaSearch = window.MirelaSearch || {}; 1398 1399 function getLocationController(reason) { 1400 try { 1401 var uiControllers = root.searchUiControllers || null; 1402 return uiControllers && typeof uiControllers.createLocationFilterController === 'function' 1403 ? uiControllers.createLocationFilterController({ reason: reason || 'locationFormLegacyCompat' }) 1404 : null; 1405 } catch (_e) { 1406 return null; 1407 } 1408 } 1409 1410 function getRequestApi() { 1411 return window.MirelaLocationSelectionRequest || null; 1412 } 1413 1414 function getHomePolicyApi() { 1415 return window.MirelaHomeLocationNavigationPolicy || null; 1416 } 1417 1418 function getNormalizedCityId(value) { 1419 var requestApi = getRequestApi(); 1420 if (requestApi && typeof requestApi.getNormalizedCityId === 'function') { 1421 return requestApi.getNormalizedCityId(value); 1422 } 1423 return ''; 1424 } 1425 1426 function toCsv(value) { 1427 var requestApi = getRequestApi(); 1428 if (requestApi && typeof requestApi.toCsv === 'function') { 1429 return requestApi.toCsv(value); 1430 } 1431 return ''; 1432 } 1433 1434 function ensureHiddenInput($form, name) { 1435 try { 1436 if (!$form || !$form.length || !window.jQuery) return null; 1437 var $element = $form.find('[name="' + name + '"]'); 1438 if (!$element.length) { 1439 $element = window.jQuery('<input type="hidden"/>').attr('name', name).appendTo($form); 1440 } 1441 return $element; 1442 } catch (_e) { 1443 return null; 1444 } 1445 } 1446 1447 function ensureHiddenInputElement(form, name) { 1448 try { 1449 if (!form || !form.querySelectorAll) return null; 1450 var nodes = Array.prototype.slice.call(form.querySelectorAll('input[name="' + name + '"]')); 1451 var field = nodes.length ? nodes[0] : null;
1452 nodes.slice(1).forEach(function (node) { 1453 try { node.parentNode && node.parentNode.removeChild(node); } catch (_eNode) {} 1454 }); 1455 if (!field) { 1456 field = document.createElement('input'); 1457 field.type = 'hidden'; 1458 field.name = name; 1459 form.appendChild(field); 1460 } 1461 return field; 1462 } catch (_e) { 1463 return null; 1464 } 1465 } 1466 1467 function setSingleHiddenValue(form, name, value) { 1468 try { 1469 var field = ensureHiddenInputElement(form, name); 1470 if (field) { 1471 field.value = String(value == null ? '' : value); 1472 } 1473 return field; 1474 } catch (_e) { 1475 return null; 1476 } 1477 } 1478 1479 function removeLocationArrayInputs(form, opts) { 1480 var options = opts || {}; 1481 try { 1482 if (!form || !form.querySelectorAll) return; 1483 var names = []; 1484 if (options.includeDistrict !== false) names.push('district_id[]'); 1485 if (options.includeDistrictGroup !== false) names.push('district_group_id[]'); 1486 if (options.includeCity === true) names.push('city_id[]'); 1487 if (options.includeStreet === true) names.push('street_id[]'); 1488 1489 names.forEach(function (name) { 1490 Array.prototype.slice.call(form.querySelectorAll('input[name="' + name + '"]')).forEach(function (node) { 1491 try { node.parentNode && node.parentNode.removeChild(node); } catch (_eNode) {} 1492 }); 1493 }); 1494 1495 try { 1496 Array.from(form.querySelectorAll('input')).forEach(function (node) { 1497 var inputName = String((node && node.name) || ''); 1498 if (/^(city_id|district_id|district_group_id|street_id)\[\d+\]$/.test(inputName)) { 1499 if ((options.includeCity === true && inputName.indexOf('city_id[') === 0) || 1500 ((options.includeDistrict !== false || options.includeDistrictGroup !== false || options.includeStreet === true) && inputName !== '')) { 1501 try { node.parentNode && node.parentNode.removeChild(node); } catch (_eIndexed) {} 1502 } 1503 } 1504 }); 1505 } catch (_eIdx) {} 1506 } catch (_e) {} 1507 } 1508 1509 function setFieldValue($form, selectors, name, value, ensureHidden) { 1510 try { 1511 var safeValue = (value == null) ? '' : String(value); 1512 if (ensureHidden) { 1513 var $hidden = ensureHiddenInput($form, name); 1514 if ($hidden && $hidden.length) { 1515 $hidden.val(safeValue); 1516 } 1517 } 1518 if ($form && $form.length && selectors) { 1519 $form.find(selectors).val(safeValue); 1520 } 1521 } catch (_e) {} 1522 } 1523 1524 function removeHiddenInputsByNames(form, names) { 1525 try { 1526 if (!form || !form.querySelectorAll) return; 1527 (names || []).forEach(function (name) { 1528 var key = String(name || '').trim(); 1529 if (!key) return; 1530 Array.prototype.slice.call(form.querySelectorAll('input[name="' + key + '"]')).forEach(function (node) { 1531 try { node.parentNode && node.parentNode.removeChild(node); } catch (_eNode) {} 1532 }); 1533 }); 1534 } catch (_e) {} 1535 } 1536 1537 function clearCommittedLocationContext($form, opts) { 1538 var options = opts || {}; 1539 try { 1540 if (!$form || !$form.length) return; 1541 var formNode = $form.get(0); 1542 if (formNode) { 1543 removeLocationArrayInputs(formNode, { 1544 includeDistrict: true, 1545 includeDistrictGroup: true, 1546 includeCity: true, 1547 includeStreet: true 1548 }); 1549 removeHiddenInputsByNames(formNode, options.removeMapManual === false ? [] : ['map_manual']); 1550 } 1551 1552 if (options.includeSearch !== false) { 1553 setFieldValue($form, '.js-location-dd-input, .js-search-input, [name="s"]', 's', '', true); 1554 } 1555 setFieldValue($form, options.citySelectors || '.js-city-id, .js-mobile-city-id, [name="city_id"]', 'city_id', '', true); 1556 setFieldValue($form, options.districtSelectors || '.js-district-id, [name="district_id"]', 'district_id', '', true); 1557 setFieldValue($form, options.groupSelectors || '.js-district-group-id, [name="district_group_id"]', 'district_group_id', '', true); 1558 setFieldValue($form, options.streetSelectors || '.js-street
1558-id, [name="street_id"]', 'street_id', '', true); 1559 1560 if (options.clearMap !== false) { 1561 setFieldValue($form, options.mapSelectors || '.js-location-map, [name="map"]', 'map', '', true); 1562 } 1563 1564 try { 1565 var locationController = getLocationController('locationFormLegacyCompat.clearCommittedLocationContext'); 1566 if (locationController && typeof locationController.applySelection === 'function') { 1567 locationController.applySelection({ 1568 searchLabel: '', 1569 cityId: '', 1570 districtId: '', 1571 groupId: '', 1572 streetId: '', 1573 districtIds: [], 1574 groupIds: [], 1575 value: null, 1576 clearMap: options.clearMap !== false 1577 }); 1578 } 1579 } catch (_eState) {} 1580 } catch (_e) {} 1581 } 1582 1583 function applySelectionToJqueryForm($form, value, opts) { 1584 var options = opts || {}; 1585 var normalizedCityId = getNormalizedCityId(value); 1586 var mode = 'empty'; 1587 1588 try { 1589 if (!$form || !$form.length) { 1590 return { cityId: normalizedCityId, mode: mode }; 1591 } 1592 1593 try { 1594 var hasLocationPick = !!( 1595 value && 1596 (normalizedCityId || value.districts || value.district_groups || value.street_id) 1597 ); 1598 if (hasLocationPick && window.isHomePage === true) { 1599 try { 1600 if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapHome === 'function') { 1601 window.MirelaSearch.applyUserMovedMapHome(false); 1602 } else { 1603 window.__mirelaUserMovedMapHome = false; 1604 } 1605 } catch (_eMovedHome) {} 1606 1607 var homePolicyApi = getHomePolicyApi(); 1608 if (homePolicyApi && typeof homePolicyApi.setHomeListIntent === 'function') { 1609 homePolicyApi.setHomeListIntent('location'); 1610 } 1611 } 1612 } catch (_eHomeGesture) {} 1613 1614 clearCommittedLocationContext($form, { 1615 includeSearch: options.searchLabel !== undefined, 1616 clearMap: options.clearMap !== false, 1617 citySelectors: options.citySelectors, 1618 districtSelectors: options.districtSelectors, 1619 groupSelectors: options.groupSelectors, 1620 streetSelectors: options.streetSelectors, 1621 mapSelectors: options.mapSelectors 1622 }); 1623 1624 if (options.searchLabel !== undefined) { 1625 setFieldValue($form, '', 's', String(options.searchLabel || ''), true); 1626 } 1627 1628 setFieldValue($form, options.citySelectors || '.js-city-id, [name="city_id"]', 'city_id', normalizedCityId, !!options.ensureCityHidden); 1629 1630 if (value && value.districts) { 1631 mode = 'districts'; 1632 setFieldValue($form, options.districtSelectors || '.js-district-id, [name="district_id"]', 'district_id', toCsv(value.districts), !!options.ensureLocationHidden); 1633 setFieldValue($form, options.groupSelectors || '.js-district-group-id, [name="district_group_id"]', 'district_group_id', '', !!options.ensureLocationHidden); 1634 setFieldValue($form, options.streetSelectors || '.js-street-id, [name="street_id"]', 'street_id', '', !!options.ensureLocationHidden); 1635 } else if (value && value.district_groups) { 1636 mode = 'district_groups'; 1637 setFieldValue($form, options.groupSelectors || '.js-district-group-id, [name="district_group_id"]', 'district_group_id', toCsv(value.district_groups), !!options.ensureLocationHidden); 1638 setFieldValue($form, options.districtSelectors || '.js-district-id, [name="district_id"]', 'district_id', '', !!options.ensureLocationHidden); 1639 setFieldValue($form, options.streetSelectors || '.js-street
1639-id, [name="street_id"]', 'street_id', '', !!options.ensureLocationHidden); 1640 } else if (value && value.street_id) { 1641 mode = 'street'; 1642 setFieldValue($form, options.districtSelectors || '.js-district-id, [name="district_id"]', 'district_id', '', !!options.ensureLocationHidden); 1643 setFieldValue($form, options.groupSelectors || '.js-district-group-id, [name="district_group_id"]', 'district_group_id', '', !!options.ensureLocationHidden); 1644 setFieldValue($form, options.streetSelectors || '.js-street-id, [name="street_id"]', 'street_id', String(value.street_id || ''), !!options.ensureLocationHidden); 1645 } else if (normalizedCityId) { 1646 mode = 'city'; 1647 setFieldValue($form, options.districtSelectors || '.js-district-id, [name="district_id"]', 'district_id', '', !!options.ensureLocationHidden); 1648 setFieldValue($form, options.groupSelectors || '.js-district-group-id, [name="district_group_id"]', 'district_group_id', '', !!options.ensureLocationHidden); 1649 setFieldValue($form, options.streetSelectors || '.js-street-id, [name="street_id"]', 'street_id', '', !!options.ensureLocationHidden); 1650 } 1651 1652 if (options.clearMap) { 1653 try { $form.find(options.mapSelectors || '.js-location-map, [name="map"]').val(''); } catch (_eClearMap) {} 1654 } 1655 if (options.persistCityPreset) { 1656 try { window.__mirelaPresetCityIdTransient = String(normalizedCityId || ''); } catch (_ePreset) {} 1657 } 1658 if (options.forceCityOnce) { 1659 try { window.__forceCityIdOnce = String(normalizedCityId || ''); } catch (_eForce) {} 1660 } 1661 1662 try { 1663 var locationController = getLocationController('locationFormLegacyCompat.applySelectionToJqueryForm'); 1664 if (locationController && typeof locationController.applySelection === 'function') { 1665 locationController.applySelection({ 1666 searchLabel: options.searchLabel !== undefined ? String(options.searchLabel || '') : '', 1667 cityId: normalizedCityId, 1668 districtId: value && value.districts ? toCsv(value.districts) : '', 1669 groupId: value && value.district_groups ? toCsv(value.district_groups) : '', 1670 streetId: value && value.street_id ? String(value.street_id || '') : '', 1671 districtIds: value && value.districts ? [].concat(value.districts).map(String) : [], 1672 groupIds: value && value.district_groups ? [].concat(value.district_groups).map(String) : [], 1673 value: value || null, 1674 clearMap: options.clearMap === true 1675 }); 1676 } 1677 } catch (_eStatePatch) {} 1678 } catch (_e) {} 1679 1680 return { 1681 cityId: normalizedCityId, 1682 mode: mode 1683 }; 1684 } 1685 1686 function hasLocationFiltersInParams(params, opts) { 1687 var usp = params instanceof URLSearchParams ? params : new URLSearchParams(String(params || '')); 1688 var options = opts || {}; 1689 try { 1690 var hasStrict = !!( 1691 String(usp.get('district_id') || '').trim() !== '' || 1692 String(usp.get('district_group_id') || '').trim() !== '' || 1693 String(usp.get('street_id') || '').trim() !== '' || 1694 (usp.getAll('district_id[]') || []).length > 0 || 1695 (usp.getAll('district_group_id[]') || []).length > 0 1696 ); 1697 if (hasStrict) return true; 1698 if (options.includeCity) { 1699 if (String(usp.get('city_id') || '').trim() !== '' || (usp.getAll('city_id[]') || []).length > 0) { 1700 return true; 1701 } 1702 } 1703 if (options.includeMap) { 1704 if (String(usp.get('map') || '').trim() !== '') { 1705 return true; 1706 } 1707 } 1708 } catch (_e) {} 1709 return false; 1710 } 1711 1712 api.ensureHiddenInput = ensureHiddenInput; 1713 api.ensureHiddenInputElement = ensureHiddenInputElement; 1714 api.setSingleHiddenValue = setSingleHiddenValue; 1715 api.removeLocationArrayInputs = removeLocationArrayInputs; 1716 api.clearCommittedLocationContext = clearCommittedLocationContext; 1717 api.applySelectionToJqueryForm = applySelectionToJqueryForm; 1718 api.hasLocationFiltersInParams = hasLocationFiltersInParams; 1719})(window); 1720 1721// ---- END: js/components/location/location-form-legacy-compat.js ---- 1722 1723// ---- START: js/components/search-location-form-utils.js ---- 1724/* Thin compatibility facade around split location modules. */ 1725(function (window) { 1726 'use strict'; 1727 1728 var root = window.MirelaSearch = window.MirelaSearch || {}; 1729 var api = root.locationFormUtils = root.locationFormUtils || {}; 1730 1731 function getRequestApi() { 1732 return window.MirelaLocationSelectionRequest || null; 1733 } 1734 1735 function getHomePolicyApi() { 1736 return window.MirelaHomeLocationNavigationPolicy || null; 1737 } 1738 1739 function getLegacyCompatApi() { 1740 return window.MirelaLocationFormLegacyCompat || null; 1741 } 1742 1743 function bindMethod(methodName, fallbackValue) { 1744 return function () { 1745 var requestApi = getRequestApi(); 1746 if (requestApi && typeof requestApi[methodName] === 'function') { 1747 return requestApi[methodName].apply(requestApi, arguments); 1748 } 1749 var homePolicyApi = getHomePolicyApi(); 1750 if (homePolicyApi && typeof homePolicyApi[methodName] === 'function') { 1751 return homePolicyApi[methodName].apply(homePolicyApi, arguments); 1752 } 1753 var legacyCompatApi = getLegacyCompatApi(); 1754 if (legacyCompatApi && typeof legacyCompatApi[methodName] === 'function') { 1755 return legacyCompatApi[methodName].apply(legacyCompatApi, arguments); 1756 } 1757 return fallbackValue; 1758 }; 1759 } 1760 1761 api.getNormalizedCityId = bindMethod('getNormalizedCityId', ''); 1762 api.toCsv = bindMethod('toCsv', ''); 1763 api.ensureHiddenInput = bindMethod('ensureHiddenInput', null); 1764 api.ensureHiddenInputElement = bindMethod('ensureHiddenInputElement', null); 1765 api.setSingleHiddenValue = bindMethod('setSingleHiddenValue', null); 1766 api.removeLocationArrayInputs = bindMethod('removeLocationArrayInputs', undefined); 1767 api.clearCommittedLocationContext = bindMethod('clearCommittedLocationContext', undefined); 1768 api.applySelectionToJqueryForm = bindMethod('applySelectionToJqueryForm', { cityId: '', mode: 'empty' }); 1769 api.buildSelectionRequestData = bindMethod('buildSelectionRequestData', { cityId: '', mode: 'empty', ajaxData: {} }); 1770 api.hasLocationFiltersInParams = bindMethod('hasLocationFiltersInParams', false); 1771 api.hasMapBoundsValue = bindMethod('hasMapBoundsValue', false); 1772 api.shouldDropCityForBounds = bindMethod('shouldDropCityForBounds', false);
1773 api.paramObjLocksHomeListToMapViewport = bindMethod('paramObjLocksHomeListToMapViewport', false); 1774 api.clearHomeMapIntentFlags = bindMethod('clearHomeMapIntentFlags', undefined); 1775 api.setHomeListIntent = bindMethod('setHomeListIntent', ''); 1776 api.getHomeListNavigationState = bindMethod('getHomeListNavigationState', null); 1777})(window); 1778 1779// ---- END: js/components/search-location-form-utils.js ---- 1780 1781// ---- START: js/components/deal-type-dd-shared.js ---- 1782/* Centralized deal-type (.deal-type-dd) nice-select: maps, UI sync, type-field sync, full-page redirect helper. */ 1783(function (window, document) { 1784 'use strict'; 1785 1786 var root = window.MirelaSearch = window.MirelaSearch || {}; 1787 var api = root.dealTypeDd = root.dealTypeDd || {}; 1788 1789 var SEL = { 1790 option: '.deal-type-dd .group-select .nice-select .option', 1791 niceSelectRoot: '.deal-type-dd .nice-select' 1792 }; 1793 1794 var DATA_TO_TYPE = { villa: '1', studio: '2', office: '3', house: '4' }; 1795 var TYPE_TO_DATA = { '1': 'villa', '2': 'studio', '3': 'office', '4': 'house' }; 1796 var DATA_TO_PATH = { 1797 villa: '/prodazhbi', 1798 studio: '/naemi', 1799 office: '/kupuvachi', 1800 house: '/naemateli' 1801 }; 1802 1803 api.selectors = SEL; 1804 api.getTypeForDataValue = function (v) { return DATA_TO_TYPE[String(v)] || ''; }; 1805 api.getDataValueForType = function (t) { return TYPE_TO_DATA[String(t)] || ''; }; 1806 api.getPathSegmentForDataValue = function (v) { return DATA_TO_PATH[String(v)] || ''; }; 1807 1808 function normalizeDealTypeKey(raw) { 1809 var s = String(raw || '').trim(); 1810 if (Object.prototype.hasOwnProperty.call(DATA_TO_PATH, s)) return s; 1811 var fromNum = TYPE_TO_DATA[s]; 1812 if (fromNum && Object.prototype.hasOwnProperty.call(DATA_TO_PATH, fromNum)) return fromNum; 1813 return ''; 1814 } 1815 1816 api.normalizeDealTypeKey = normalizeDealTypeKey; 1817 1818 api.getRoutingRule = function (raw) { 1819 var k = normalizeDealTypeKey(raw); 1820 if (!k) return null; 1821 return { 1822 key: k, 1823 pathSegment: DATA_TO_PATH[k], 1824 type: DATA_TO_TYPE[k], 1825 action: (k === 'office' || k === 'house') ? 'redirect' : 'submit' 1826 }; 1827 }; 1828 1829 api.getCanonicalNumericType = function (raw) { 1830 var k = normalizeDealTypeKey(raw); 1831 return k ? (DATA_TO_TYPE[k] || '') : ''; 1832 }; 1833 1834 function setCurrentFromOption(dd, optionEl) { 1835 var current = dd && dd.querySelector ? dd.querySelector('.current') : null; 1836 if (!current || !optionEl) return; 1837 var labelEl = optionEl.querySelector ? optionEl.querySelector('.deal-type-label') : null; 1838 var label = String((labelEl || optionEl).textContent || '').trim(); 1839 var icon = optionEl.querySelector ? optionEl.querySelector('.deal-type-icon') : null; 1840 if (!icon) { 1841 current.textContent = label; 1842 return; 1843 } 1844 current.textContent = ''; 1845 var currentIcon = icon.cloneNode(false); 1846 currentIcon.setAttribute('alt', ''); 1847 currentIcon.setAttribute('aria-hidden', 'true'); 1848 currentIcon.className = 'deal-type-icon'; 1849 var currentLabel = document.createElement('span'); 1850 currentLabel.className = 'deal-type-current-label'; 1851 currentLabel.textContent = label; 1852 currentLabel.setAttribute('title', label); 1853 current.appendChild(currentIcon); 1854 current.appendChild(currentLabel); 1855 } 1856 1857 api.setCurrentFromOption = setCurrentFromOption; 1858 1859 function getFilterStateSnapshot() { 1860 try { 1861 if (typeof window.filterState === 'object' && window.filterState) { 1862 return window.filterState; 1863 } 1864 } catch (_eFilterState) { 1865 return {}; 1866 } 1867 return {}; 1868 } 1869 1870 function getCentralType() { 1871 try { 1872 var snapshot = getFilterStateSnapshot(); 1873 var normalized = String(snapshot.type || '').trim(); 1874 if (normalized === '1' || normalized === '2' || normalized === '3' || normalized === '4') { 1875 return normalized; 1876 } 1877 } catch (_eSlice) {} 1878 return ''; 1879 } 1880 1881 function getCurrentTypeFromForm(formNative) { 1882 try { 1883 var form = formNative || document.querySelector('.js-search-form'); 1884 if (form && form.querySelector) { 1885 var input = form.querySelector('input[name="type"], select[name="type"], .js-type-input, .js-mobile-type-input'); 1886 var formType = String(input && input.value != null ? input.value : '').trim(); 1887 if (formType === '1' || formType === '2' || formType === '3' || formType === '4') { 1888 return formType; 1889 } 1890 } 1891 } catch (_eFormType) {} 1892 return getCentralType(); 1893 } 1894 1895 api.applyOptionVisual = function (optionEl, opts) { 1896 opts = opts || {}; 1897 var closeDropdown = opts.closeDropdown === true; 1898 try { 1899 var dd = optionEl && optionEl.closest ? optionEl.closest('.nice-select') : null; 1900 if (!dd) return; 1901 dd.querySelectorAll('.option').forEach(function (li) { li.classList.remove('selected'); }); 1902 optionEl.classList.add('selected'); 1903 setCurrentFromOption(dd, optionEl); 1904 if (closeDropdown) { 1905 dd.classList.remove('open'); 1906 var wrap = dd.closest('.deal-type-dd'); 1907 if (wrap && wrap.classList.contains('open')) wrap.classList.remove('open'); 1908 } 1909 } catch (_e) {} 1910 }; 1911 1912 api.setTypeInputValue = function (typeNum, opts) { 1913 opts = opts || {}; 1914 var next = String(typeNum || '').trim(); 1915 if (!next) return; 1916 try { 1917 if (typeof window.changeTypeState === 'function') { 1918 window.changeTypeState(next); 1919 } 1920 } catch (_eStateFn) {} 1921 try { 1922 document.querySelectorAll('input[name="type"], select[name="type"], .js-type-input, .js-mobile-type-input').forEach(function (el) { 1923 if (!el) return; 1924 if (opts.onlyIfEmpty === true && String(el.value || '').trim() !== '') return; 1925 el.value = next; 1926 try { 1927 el.dispatchEvent(new Event('change', { bubbles: true })); 1928 } catch (_eDispatch) {} 1929 }); 1930 } catch (_eDom) {} 1931 try { 1932 var $ = window.jQuery; 1933 if (!$) return; 1934 $('input[name="type"], select[name="type"], .js-type-input, .js-mobile-type-input').each(function () { 1935 if (opts.onlyIfEmpty === true && String($(this).val() || '').trim() !== '') return; 1936 $(this).val(next).trigger('change'); 1937 }); 1938 } catch (_eJq) {} 1939 }; 1940
1941 api.setJqueryTypeInputValue = function (typeNum) { 1942 api.setTypeInputValue(typeNum); 1943 }; 1944 1945 api.syncInitialNiceSelectFromDom = function (opts) { 1946 opts = opts || {}; 1947 var dd = document.querySelector(opts.rootSelector || SEL.niceSelectRoot); 1948 if (!dd) return; 1949 var centralType = getCentralType(); 1950 if (centralType) { 1951 api.syncAllNiceSelectsForNumericType(centralType); 1952 if (opts.fillEmptyTypeInput !== false) { 1953 api.setTypeInputValue(centralType, { onlyIfEmpty: true }); 1954 } 1955 return; 1956 } 1957 var selected = dd.querySelector('.option.selected') || dd.querySelector('.option'); 1958 if (selected && !selected.classList.contains('selected')) selected.classList.add('selected'); 1959 if (selected) setCurrentFromOption(dd, selected); 1960 if (opts.fillEmptyTypeInput === false) return; 1961 var val = selected ? selected.getAttribute('data-value') : 'villa'; 1962 var typeStr = DATA_TO_TYPE[val] || '1'; 1963 api.setTypeInputValue(typeStr, { onlyIfEmpty: true }); 1964 }; 1965 1966 api.syncAllNiceSelectsForNumericType = function (typeNum) { 1967 var normalizedType = String(typeNum || '').trim(); 1968 try { window.__mobileModalDealTypeChoice = normalizedType; } catch (_eChoice) {} 1969 var dv = TYPE_TO_DATA[normalizedType]; 1970 if (!dv) return; 1971 try { 1972 document.querySelectorAll('.deal-type-dd .nice-select').forEach(function (ns) { 1973 ns.querySelectorAll('.option').forEach(function (o) { 1974 o.classList.remove('selected'); 1975 if (o.getAttribute('data-value') === dv) { 1976 o.classList.add('selected'); 1977 setCurrentFromOption(ns, o); 1978 } 1979 }); 1980 }); 1981 } catch (_e) {} 1982 }; 1983 1984 function serializeFormExcluding($form, excludeKeys) { 1985 excludeKeys = excludeKeys || []; 1986 try { 1987 var fq = root.formQueryUtils; 1988 if (fq && typeof fq.serializeJqueryForm === 'function') { 1989 return fq.serializeJqueryForm($form, { remove: excludeKeys }); 1990 } 1991 } catch (_e) {} 1992 try { 1993 var $ = window.jQuery; 1994 if (!$form || !$form.serializeArray) return ''; 1995 return $form.serializeArray() 1996 .filter(function (i) { 1997 return i && i.name !== '_token' && excludeKeys.indexOf(i.name) === -1 && String(i.value || '').trim() !== ''; 1998 }) 1999 .map(function (i) { return encodeURIComponent(i.name) + '=' + encodeURIComponent(i.value); }) 2000 .join('&'); 2001 } catch (_e2) { 2002 return ''; 2003 } 2004 } 2005 2006 api.serializeFormExcluding = serializeFormExcluding; 2007 2008 function getFormHandles(opts) { 2009 opts = opts || {}; 2010 var getForm = typeof opts.getForm === 'function' 2011 ? opts.getForm 2012 : function () { 2013 return window.jQuery ? window.jQuery('.js-search-form') : null; 2014 }; 2015 var jqForm = null; 2016 try { 2017 jqForm = getForm(); 2018 } catch (_eGetForm) { 2019 jqForm = null; 2020 } 2021 var formNative = null; 2022 try { 2023 if (jqForm && jqForm.jquery) { 2024 formNative = jqForm.get(0) || null; 2025 } else if (jqForm && jqForm.nodeType === 1) { 2026 formNative = jqForm; 2027 jqForm = null; 2028 } 2029 } catch (_eNative) {} 2030 if (!formNative) { 2031 try { 2032 formNative = document.querySelector('.js-search-form'); 2033 } catch (_eDom) {} 2034 } 2035 return { 2036 formNative: formNative || null, 2037 formJquery: (jqForm && jqForm.jquery) ? jqForm : null 2038 }; 2039 } 2040 2041 function handleNavigationResult(navDeal) { 2042 if (!navDeal || !navDeal.handled) return false; 2043 try { 2044 if (!navDeal.sync && navDeal.promise && typeof navDeal.promise.catch === 'function') { 2045 navDeal.promise.catch(function () {}); 2046 } 2047 } catch (_eNavCatch) {} 2048 return true; 2049 } 2050 2051 function defaultNavigateFromForm(formNative) { 2052 try { 2053 var ms = window.MirelaSearch; 2054 var navDeal = 2055 ms && typeof ms.tryOffersSearchServerListNavigationFromForm === 'function' && formNative 2056 ? ms.tryOffersSearchServerListNavigationFromForm(formNative, { 2057 removeKeys: ['page'] 2058 }) 2059 : null; 2060 return handleNavigationResult(navDeal); 2061 } catch (_eNav) { 2062 return false;
2063 } 2064 } 2065 2066 api.handleSelection = function (optionEl, opts) { 2067 opts = opts || {}; 2068 if (!optionEl) return false; 2069 var val = String(optionEl.getAttribute('data-value') || '').trim(); 2070 var rule = api.getRoutingRule(val); 2071 if (!rule || !rule.pathSegment) return false; 2072 2073 var formHandles = getFormHandles(opts); 2074 var ctx = { 2075 optionEl: optionEl, 2076 value: val, 2077 rule: rule, 2078 typeNum: String(rule.type || api.getCanonicalNumericType(val) || '1'), 2079 previousType: getCurrentTypeFromForm(formHandles.formNative), 2080 formNative: formHandles.formNative, 2081 formJquery: formHandles.formJquery, 2082 mode: String(opts.mode || '').trim().toLowerCase() || 'navigate' 2083 }; 2084 2085 api.applyOptionVisual(optionEl, { closeDropdown: opts.closeDropdown !== false }); 2086 api.setTypeInputValue(ctx.typeNum, { dispatchDomEvents: true, forceNotify: true }); 2087 2088 if (typeof opts.onBeforeAction === 'function') { 2089 try { 2090 if (opts.onBeforeAction(ctx) === false) return true; 2091 } catch (_eBefore) {} 2092 } 2093 2094 if (ctx.mode === 'list-ajax') { 2095 if (typeof opts.onListAjax === 'function') { 2096 try { 2097 opts.onListAjax(ctx); 2098 return true; 2099 } catch (_eList) { 2100 return true; 2101 } 2102 } 2103 return true; 2104 } 2105 2106 if (typeof opts.onNavigate === 'function') { 2107 try { 2108 var navResult = opts.onNavigate(ctx); 2109 if (navResult === true) return true; 2110 if (navResult && typeof navResult === 'object') { 2111 return handleNavigationResult(navResult); 2112 } 2113 } catch (_eCustomNav) {} 2114 } 2115 2116 return defaultNavigateFromForm(ctx.formNative); 2117 }; 2118 2119 api.installSelectionRouting = function (opts) { 2120 opts = opts || {}; 2121 var key = String(opts.bindingKey || opts.mode || 'default').trim() || 'default'; 2122 window.__mirelaDealTypeDdBindings = window.__mirelaDealTypeDdBindings || {}; 2123 if (window.__mirelaDealTypeDdBindings[key]) return; 2124 window.__mirelaDealTypeDdBindings[key] = true; 2125 2126 document.addEventListener('click', function (ev) { 2127 var opt = ev && ev.target && ev.target.closest ? ev.target.closest(SEL.option) : null; 2128 if (!opt) return; 2129 if (typeof opts.shouldHandle === 'function') { 2130 try { 2131 if (opts.shouldHandle(opt, ev) === false) return; 2132 } catch (_eShouldHandle) {} 2133 } 2134 try { 2135 ev.preventDefault(); 2136 ev.stopPropagation(); 2137 if (ev.stopImmediatePropagation) ev.stopImmediatePropagation(); 2138 } catch (_eCancel) {} 2139 api.handleSelection(opt, opts); 2140 }, true); 2141 }; 2142 2143 api.installFullPageRedirectListener = function (opts) { 2144 opts = opts || {}; 2145 opts.mode = 'navigate'; 2146 opts.bindingKey = opts.bindingKey || 'full-page'; 2147 api.installSelectionRouting(opts); 2148 }; 2149 2150 if (!window.__mirelaDealTypeDdDomSyncBound) { 2151 window.__mirelaDealTypeDdDomSyncBound = true; 2152 // Bubble (not capture): installSelectionRouting handlers use capture + stopPropagation, 2153 // so they run first with an uncorrupted previous type. Capture-phase sync here ran before 2154 // late-bound routing (e.g. demands-submit) and broke price-clear transitions (4â3). 2155 document.addEventListener('click', function (ev) { 2156 var opt = ev && ev.target && ev.target.closest ? ev.target.closest(SEL.option) : null; 2157 if (!opt) return; 2158 api.applyOptionVisual(opt, { closeDropdown: false }); 2159 api.setTypeInputValue(api.getCanonicalNumericType(opt.getAttribute('data-value')) || '1', { dispatchDomEvents: true, forceNotify: true }); 2160 }); 2161 } 2162})(window, document); 2163 2164// ---- END: js/components/deal-type-dd-shared.js ---- 2165 2166// ---- START: js/components/mirela-search-call-utils.js ---- 2167/* Safe resolve/call helpers for MirelaSearch modules. */ 2168(function (window) { 2169 'use strict'; 2170 2171 var root = window.MirelaSearch = window.MirelaSearch || {}; 2172 var api = root.callUtils = root.callUtils || {}; 2173 2174 function getModule(name) { 2175 try { 2176 var key = (name == null) ? '' : String(name).trim(); 2177 if (!key) return null; 2178 var mod = root[key]; 2179 return (mod && typeof mod === 'object') ? mod : null; 2180 } catch (_e) { 2181 return null; 2182 } 2183 } 2184 2185 function call(moduleName, methodName, args, fallback) { 2186 try { 2187 var mod = getModule(moduleName); 2188 var method = (mod && methodName != null) ? mod[String(methodName)] : null; 2189 if (typeof method === 'function') { 2190 var list = Array.isArray(args) ? args : []; 2191 return method.apply(mod, list); 2192 } 2193 } catch (_e) {} 2194 return fallback; 2195 } 2196 2197 function callBool(moduleName, methodName, args, fallback) { 2198 return !!call(moduleName, methodName, args, !!fallback); 2199 } 2200 2201 api.getModule = getModule; 2202 api.call = call; 2203 api.callBool = callBool; 2204})(window); 2205 2206 2207// ---- END: js/components/mirela-search-call-utils.js ---- 2208 2209// ---- START: js/api/send-map-browse-offers.js ---- 2210'use strict'; 2211 2212(function (window) { 2213 'use strict'; 2214 2215 var root = window.MirelaSearch = window.MirelaSearch || {}; 2216 var api = root.mapBrowseOffersApi = root.mapBrowseOffersApi || {}; 2217 2218 function trim(value) { 2219 return String(value == null ? '' : value).trim(); 2220 } 2221 2222 function cloneEntries(entries) { 2223 return (Array.isArray(entries) ? entries : []).reduce(function (acc, entry) { 2224 if (!entry || !entry.name) return acc; 2225 acc.push({ 2226 name: String(entry.name), 2227 value: String(entry.value == null ? '' : entry.value) 2228 }); 2229 return acc; 2230 }, []); 2231 } 2232 2233 function normalizePayload(input) { 2234 if (typeof input === 'string') { 2235 return trim(input); 2236 } 2237 if (Array.isArray(input)) { 2238 return cloneEntries(input); 2239 } 2240 if (input && typeof input === 'object') { 2241 var payload = {}; 2242 Object.keys(input).forEach(function (key) { 2243 if (!trim(key)) return; 2244 payload[key] = input[key]; 2245 }); 2246 return payload; 2247 } 2248 return ''; 2249 } 2250 2251 function appendEntry(target, name, value) { 2252 var normalizedName = trim(name); 2253 var normalizedValue = value == null ? '' : String(value); 2254 if (!normalizedName || !trim(normalizedValue)) return;
2255 target.push({ name: normalizedName, value: normalizedValue }); 2256 } 2257 2258 function appendArrayEntries(target, name, list) { 2259 (Array.isArray(list) ? list : []).forEach(function (value) { 2260 appendEntry(target, name + '[]', value); 2261 }); 2262 } 2263 2264 function appendMultiValueEntries(target, name, value) { 2265 if (Array.isArray(value)) { 2266 appendArrayEntries(target, name, value); 2267 return; 2268 } 2269 var raw = trim(value); 2270 if (!raw) return; 2271 if (raw.indexOf(',') !== -1 || raw.indexOf('|') !== -1) { 2272 raw.split(/[|,]/).forEach(function (item) { 2273 appendEntry(target, name + '[]', item); 2274 }); 2275 return; 2276 } 2277 appendEntry(target, name + '[]', raw); 2278 } 2279 2280 function buildPayloadFromState(options) { 2281 var opts = options || {}; 2282 var filterSnapshot = (typeof window.filterState === 'object' && window.filterState) ? window.filterState : null; 2283 var builder = root.searchRequestBuilder; 2284 var entries = []; 2285 2286 if (builder && typeof builder.serializeSnapshotToEntries === 'function') { 2287 entries = builder.serializeSnapshotToEntries(filterSnapshot, '') || []; 2288 } else if (filterSnapshot && typeof filterSnapshot === 'object') { 2289 [ 2290 'type', 's', 'city_id', 'street_id', 2291 'price_from', 'price_to', 'area_from', 'area_to', 2292 'floor_from', 'floor_to', 'is_top_floor', 'bedrooms', 'bathrooms', 2293 'sea_side', 'has_upkeep', 'yearround_use', 2294 'personal', 'has_project', 'new_building', 'any_parking', 2295 'no_transition', 'guard', 'elevator', 'furnishing', 2296 'no_furnishing', 'pets_allowed', 'map', 'map_manual', 'zoom' 2297 ].forEach(function (key) { 2298 appendEntry(entries, key, filterSnapshot[key]); 2299 }); 2300 [ 2301 'district_id', 2302 'district_group_id', 2303 'property_type', 2304 'focus', 2305 'construction_type', 2306 'construction_stage', 2307 'land_only' 2308 ].forEach(function (key) { 2309 appendMultiValueEntries(entries, key, filterSnapshot[key]); 2310 }); 2311 } 2312 2313 if (!entries.length) { 2314 return ''; 2315 } 2316 2317 if (opts.page != null) { 2318 appendEntry(entries, 'page', opts.page); 2319 } 2320 2321 if (opts.extraEntries && typeof opts.extraEntries === 'object') { 2322 if (Array.isArray(opts.extraEntries)) { 2323 entries = entries.concat(cloneEntries(opts.extraEntries)); 2324 } else { 2325 Object.keys(opts.extraEntries).forEach(function (key) { 2326 var value = opts.extraEntries[key]; 2327 if (Array.isArray(value)) { 2328 value.forEach(function (item) { 2329 appendEntry(entries, /\[\]$/.test(key) ? key : key + '[]', item); 2330 }); 2331 return; 2332 } 2333 appendEntry(entries, key, value); 2334 }); 2335 } 2336 } 2337 2338 return entries; 2339 } 2340 2341 function mapBrowseOffersEndpoint() { 2342 try { 2343 var m = String((window.location && window.location.pathname) || '') 2344 .match(/^\/(en|ru|de)(?=\/|$)/i); 2345 return (m ? '/' + m[1].toLowerCase() : '') + '/map-browse-offers'; 2346 } catch (_e) { 2347 return '/map-browse-offers'; 2348 } 2349 } 2350 2351 function sendMapBrowseOffers(options) { 2352 var opts = options || {}; 2353 var hasExplicitPayload = 2354 Object.prototype.hasOwnProperty.call(opts, 'payload') && 2355 opts.payload !== undefined && 2356 opts.payload !== null; 2357 var payload = normalizePayload( 2358 hasExplicitPayload ? opts.payload : buildPayloadFromState(opts) 2359 ); 2360 2361 if (typeof window.jQuery === 'undefined' || typeof window.jQuery.post !== 'function') { 2362 return null; 2363 } 2364 2365 var xhr; 2366 if (typeof opts.success === 'function') { 2367 xhr = window.jQuery.post(mapBrowseOffersEndpoint(), payload, opts.success, opts.dataType); 2368 } else { 2369 xhr = window.jQuery.post(mapBrowseOffersEndpoint(), payload); 2370 } 2371 2372 if (xhr && typeof xhr.fail === 'function' && typeof opts.fail === 'function') { 2373 xhr.fail(opts.fail); 2374 } 2375 if (xhr && typeof xhr.always === 'function' && typeof opts.always === 'function') {
2376 xhr.always(opts.always); 2377 } 2378 return xhr; 2379 } 2380 2381 api.buildPayloadFromState = buildPayloadFromState; 2382 api.sendMapBrowseOffers = sendMapBrowseOffers; 2383 window.sendMapBrowseOffers = sendMapBrowseOffers; 2384})(window); 2385 2386// ---- END: js/api/send-map-browse-offers.js ---- 2387 2388// ---- START: js/api/map-browse-renderers.js ---- 2389'use strict'; 2390 2391(function (window) { 2392 'use strict'; 2393 2394 function noop() {} 2395 var managedMapLoader = null; 2396 2397 function callMaybe(fn, response, context) { 2398 if (typeof fn !== 'function') return undefined; 2399 return fn(response, context); 2400 } 2401 2402 function send(options, onSuccess) { 2403 var opts = options || {}; 2404 if (typeof window.sendMapBrowseOffers !== 'function') { 2405 return null; 2406 } 2407 return window.sendMapBrowseOffers({ 2408 payload: opts.payload, 2409 mapString: opts.mapString, 2410 page: opts.page, 2411 extraEntries: opts.extraEntries, 2412 dataType: opts.dataType, 2413 success: onSuccess, 2414 fail: opts.fail, 2415 always: opts.always 2416 }); 2417 } 2418 2419 function normalizeBrowseResponseForMapRender(response) { 2420 if (!response || typeof response !== 'object') return response; 2421 try { 2422 if ( 2423 response.marker_data && 2424 Array.isArray(response.marker_data) && 2425 (!response.data || !Array.isArray(response.data)) 2426 ) { 2427 return Object.assign({}, response, { data: response.marker_data }); 2428 } 2429 } catch (_e) {} 2430 return response; 2431 } 2432 2433 function defaultPinsMoveMapRender(response) { 2434 var ml = window.MapLeaflet; 2435 if (!ml || typeof ml.render !== 'function') return; 2436 var resp = normalizeBrowseResponseForMapRender(response); 2437 try { 2438 ml.render(resp); 2439 } catch (_eRender) {} 2440 if (typeof ml.fitMapBoundsSafely === 'function') { 2441 try { 2442 ml.fitMapBoundsSafely(resp); 2443 } catch (_eFit) {} 2444 } 2445 } 2446 2447 function showMapBrowseLoader() { 2448 try { 2449 if (managedMapLoader) return; 2450 var jq = window.jQuery; 2451 if (!jq) return; 2452 var container = jq('.b-browse-map').first(); 2453 if (!container.length || container.find('.overlay-loading').length) return; 2454 managedMapLoader = jq("<div class='overlay-loading' data-map-browse-render-loader='1'><div class='preload-logo'><div class='spinner'></div></div></div>").css( 2455 'pointer-events', 2456 'none' 2457 ); 2458 container.append(managedMapLoader); 2459 } catch (_e) {} 2460 } 2461 2462 function hideMapBrowseLoader() { 2463 try { 2464 if (!managedMapLoader) return; 2465 managedMapLoader.remove(); 2466 managedMapLoader = null; 2467 } catch (_e) { 2468 managedMapLoader = null; 2469 } 2470 } 2471 2472 function normalizeRenderOptions(options) { 2473 if (!options || typeof options !== 'object') return {}; 2474 return options; 2475 } 2476 2477 /** Shared page contract from filter-state.blade.php: mapOnly | mapAndList | submit */ 2478 function resolveBrowsePageType() { 2479 var pt = ''; 2480 try { 2481 pt = String(window.pageType || '').trim(); 2482 } catch (_e) { 2483 pt = ''; 2484 } 2485 if (pt === 'mapOnly' || pt === 'mapAndList' || pt === 'submit') { 2486 return pt; 2487 } 2488 return 'submit'; 2489 } 2490 2491 /** 2492 * Centralized auto-browse dispatcher for state-driven filter/location commits. 2493 * - mapOnly: pins only 2494 * - mapAndList: list entrypoint (which renders both map and list through renderFullMapBrowse path) 2495 * - submit: no-op 2496 */ 2497 function dispatchBrowseByPageType(options) { 2498 var opts = normalizeRenderOptions(options); 2499 var pt = opts.pageType || resolveBrowsePageType(); 2500 if (pt === 'submit') { 2501 return null; 2502 } 2503 if (pt === 'mapOnly') { 2504 return renderPinsMoveMap(opts.mapOnlyOptions || opts); 2505 } 2506 if (pt === 'mapAndList') { 2507 if (typeof opts.mapAndListAction === 'function') { 2508 return opts.mapAndListAction(); 2509 } 2510 if (typeof window.loadOffers === 'function') { 2511 try { 2512 if (typeof window.resetPaginationFilterState === 'function') { 2513 window.resetPaginationFilterState(); 2514 } 2515 } catch (_eResetPage) {} 2516 return window.loadOffers(1, false, true); 2517 } 2518 } 2519 return null; 2520 } 2521 2522 function renderPinsMoveMap(options) { 2523 var opts = normalizeRenderOptions(options); 2524 var isStandaloneCall = !opts.payload && typeof opts.renderMap !== 'function'; 2525 if (isStandaloneCall && opts.defer !== false) { 2526 window.setTimeout(function () { 2527 renderPinsMoveMap(Object.assign({}, opts, { defer: false })); 2528 }, 0); 2529 return null; 2530 } 2531 2532 var renderMap = typeof opts.renderMap === 'function' ? opts.renderMap : defaultPinsMoveMapRender; 2533 if (opts.loader !== false) { 2534 showMapBrowseLoader(); 2535 } 2536 2537 var xhr = send(Object.assign({}, opts, { 2538 always: function () { 2539 if (opts.loader !== false) { 2540 hideMapBrowseLoader(); 2541 } 2542 callMaybe(opts.always, null, { source: opts.source || 'map' }); 2543 } 2544 }), function (response) { 2545 var context = { 2546 payload: opts.payload, 2547 source: opts.source || 'map' 2548 }; 2549 if (callMaybe(opts.beforeRender, response, context) === false) { 2550 return; 2551 } 2552 callMaybe(renderMap || noop, response, context); 2553 callMaybe(opts.afterRender, response, context); 2554 }); 2555 if (!xhr && opts.loader !== false) { 2556 hideMapBrowseLoader(); 2557 } 2558 return xhr; 2559 } 2560 2561 function renderFullMapBrowse(options) { 2562 var opts = options || {}; 2563 return send(opts, function (response) { 2564 var context = { 2565 payload: opts.payload, 2566 source: opts.source || 'list' 2567 }; 2568 var renderMap = opts.renderMap !== false ? opts.renderMap : null; 2569 var renderList = opts.renderList !== false ? opts.renderList : null; 2570 if (callMaybe(opts.beforeRender, response, context) === false) { 2571 return; 2572 } 2573 if (opts.renderMapFirst === false) { 2574 callMaybe(renderList || noop, response, context); 2575 callMaybe(renderMap || noop, response, context); 2576 } else { 2577 callMaybe(renderMap || noop, response, context); 2578 callMaybe(renderList || noop, response, context); 2579 } 2580 callMaybe(opts.afterRender, response, context); 2581 }); 2582 } 2583 2584 window.renderPinsMoveMap = renderPinsMoveMap; 2585 window.renderFullMapBrowse = renderFullMapBrowse; 2586 window.dispatchBrowseByPageType = dispatchBrowseByPageType; 2587})(window); 2588 2589// ---- END: js/api/map-browse-renderers.js ---- 2590 2591// ---- START: js/components/map-browse-offers-ajax.js ---- 2592'use strict'; 2593 2594function makeOffersAjax(success, dataType) { 2595 try { 2596 if (typeof window.sendMapBrowseOffers === 'function') { 2597 return window.sendMapBrowseOffers({ 2598 success: success, 2599 dataType: dataType 2600 }); 2601 } 2602 } catch (_e) {} 2603 return null; 2604} 2605 2606// ---- END: js/components/map-browse-offers-ajax.js ---- 2607 2608// ---- START: js/components/search-page-context.js ---- 2609/* Single place for home / list / list-shell detection used by search bar, hydrator, and submit flows. */ 2610(function (window, document) { 2611 'use strict'; 2612 2613 var root = window.MirelaSearch = window.MirelaSearch || {}; 2614 var route = root.routeUtils || {}; 2615 2616 function getRoute() { 2617 return root.routeUtils || route; 2618 } 2619 2620 function getSearchShell() { 2621 try { 2622 var shell = window.__MIRELA_SEARCH_SHELL__; 2623 return (shell && typeof shell === 'object') ? shell : {}; 2624 } catch (_e) { 2625 return {}; 2626 } 2627 } 2628 2629 function shellFlag(name) { 2630 try { 2631 var shell = getSearchShell(); 2632 return shell && shell[name] === true; 2633 } catch (_e) { 2634 return false; 2635 } 2636 } 2637 2638 function hasServerShell() { 2639 try { 2640 return !!String(getSearchShell().pageKind || '').trim(); 2641 } catch (_e) { 2642 return false; 2643 } 2644 } 2645 2646 function isListRoutePath(path) { 2647 try { 2648 var r = getRoute(); 2649 if (r && typeof r.isListRoutePath === 'function') { 2650 return !!r.isListRoutePath(path);
2651 } 2652 } catch (_e) {} 2653 var p = String(path || '').toLowerCase(); 2654 return /^\/(?:en\/|ru\/|de\/)?(?:prodazhbi|naemi|kupuvachi|naemateli|for-sale|for-rent|prodazha|arenda|verkauf|miete)(?:\/|$)/.test(p); 2655 } 2656 2657 function detectLocale(pathname) { 2658 try { 2659 var r = getRoute(); 2660 if (r && typeof r.detectLocaleFromPath === 'function') { 2661 return String(r.detectLocaleFromPath(pathname || window.location.pathname || '') || 'bg').toLowerCase() || 'bg'; 2662 } 2663 } catch (_e) {} 2664 var first = String(String(pathname || window.location.pathname || '').split('/').filter(Boolean)[0] || '').toLowerCase(); 2665 if (first === 'en' || first === 'ru' || first === 'de') return first; 2666 return 'bg'; 2667 } 2668 2669 /** Offer preview uses list-shaped SEO paths; exclude from list URL authority and list-only UX. */ 2670 function isOfferPreviewBlockingListContext() { 2671 if (shellFlag('isPreview')) return true; 2672 try { 2673 if (window.isOfferPreviewPage === true) return true; 2674 } catch (_e) {} 2675 return false; 2676 } 2677 2678 function isOfferLikeListShapedPath(path) { 2679 try { 2680 var r = getRoute(); 2681 if (r && typeof r.isOfferLikePath === 'function') { 2682 return !!r.isOfferLikePath(path != null ? path : window.location.pathname || ''); 2683 } 2684 } catch (_e) {} 2685 return false; 2686 } 2687 2688 /** True when list XHR entry exists (window.loadOffers or searchRequest.loadOffersImpl). */ 2689 function listAjaxRuntimeReady() { 2690 try { 2691 var lr = root.listRuntime; 2692 if (lr && typeof lr.hasAjaxLoadOffers === 'function') { 2693 return !!lr.hasAjaxLoadOffers(); 2694 } 2695 } catch (_eLr) {} 2696 try { 2697 return typeof window.loadOffers === 'function'; 2698 } catch (_e2) { 2699 return false; 2700 } 2701 } 2702 2703 /** 2704 * List shell for hydration / canonical URL rules (runs before loadOffers may exist). 2705 * Path matches known list SEO routes, or SSR flag, or layout + AJAX runtime. 2706 * Excludes offer-detail URLs that share the /{deal}/⦠prefix with lists. 2707 */ 2708 function isListSearchContext() { 2709 if (hasServerShell()) return shellFlag('isList'); 2710 if (isOfferPreviewBlockingListContext()) return false; 2711 var path = String(window.location.pathname || ''); 2712 if (isOfferLikeListShapedPath(path)) return false; 2713 try { 2714 if (window.isListPage === true) return true; 2715 } catch (_e) {} 2716 try { 2717 if (document.querySelector('.wrap-left') && listAjaxRuntimeReady()) return true; 2718 } catch (_e2) {} 2719 return isListRoutePath(path); 2720 } 2721 2722 function isListSearchContextEarly() { 2723 if (hasServerShell()) return shellFlag('isList'); 2724 if (isOfferPreviewBlockingListContext()) return false; 2725 var pathEarly = String(window.location.pathname || ''); 2726 if (isOfferLikeListShapedPath(pathEarly)) return false; 2727 try { 2728 if (window.isListPage === true) return true; 2729 } catch (_e) {} 2730 try { 2731 if (document.querySelector('.wrap-left')) return true; 2732 } catch (_e2) {} 2733 return isListRoutePath(pathEarly); 2734 } 2735 2736 /** True when /map-browse-offers via loadOffers is allowed. */ 2737 function isListAjaxAvailable() { 2738 try { 2739 if (window.__disableAjaxList) return false; 2740 } catch (_e) {} 2741 try { 2742 return !!(document.querySelector('.wrap-left') && listAjaxRuntimeReady()); 2743 } catch (_e2) {} 2744 return false; 2745 } 2746 2747 /** 2748 * List page shell: left column present and either AJAX list or non-AJAX list refresh mode. 2749 * Use for ribbon submit / top-types instead of ad-hoc checks. 2750 */ 2751 function isListShellWithRefresh() { 2752 try { 2753 var scShell = root.searchSubmitController || null; 2754 if (scShell && typeof scShell.isExternalSearchPage === 'function' && scShell.isExternalSearchPage()) { 2755 return false; 2756 } 2757 } catch (_eExtShell) {} 2758 try { 2759 var hasLayout = !!document.querySelector('.wrap-left'); 2760 if (!hasLayout) return false; 2761 var hasRuntime = listAjaxRuntimeReady() || !!window.__disableAjaxList; 2762 return !!hasRuntime; 2763 } catch (_e) { 2764 return false; 2765 } 2766 } 2767 2768 function isHomePage() { 2769 if (hasServerShell()) return shellFlag('isHome'); 2770 try { 2771 if (document.querySelector('.wrap-left')) return false; 2772 var path = window.location.pathname || ''; 2773 if (isListRoutePath(path)) return false; 2774 if ((path === '/' || path === '' || /^\/(en|de|ru)\/?$/.test(path)) && !!document.querySelector('#map')) { 2775 return true; 2776 } 2777 return false; 2778 } catch (_e) { 2779 return false; 2780 } 2781 } 2782 2783 /**
2784 * "List map" page for submit controller and navigation: home is never list. 2785 * Accepts list shell before loadOffers attaches if URL is a list route or ListPageConfig is present. 2786 */ 2787 function isListMapPage() { 2788 if (hasServerShell()) return shellFlag('isList'); 2789 try { 2790 if (isHomePage()) return false; 2791 } catch (_eH) {} 2792 var pathLm = String(window.location.pathname || ''); 2793 if (isOfferLikeListShapedPath(pathLm)) return false; 2794 try { 2795 if (window.isListPage === true) return true; 2796 } catch (_eW) {} 2797 try { 2798 if (!document.querySelector('.wrap-left')) return false; 2799 } catch (_eL) { 2800 return false; 2801 } 2802 try { 2803 var path = pathLm; 2804 var onListPath = isListRoutePath(path); 2805 var hasMap = !!document.querySelector('#map'); 2806 if (!hasMap && !onListPath && !window.ListPageConfig) return false; 2807 if (listAjaxRuntimeReady()) return true; 2808 if (window.__disableAjaxList) return true; 2809 if (window.ListPageConfig) return true; 2810 if (onListPath) return true; 2811 return false; 2812 } catch (_e) { 2813 return false; 2814 } 2815 } 2816 2817 function getSearchProfileName() { 2818 try { 2819 if (isHomePage()) return 'home_map'; 2820 } catch (_eHome) {} 2821 try { 2822 if (isListAjaxAvailable()) return 'list_ajax'; 2823 } catch (_eListAjax) {} 2824 try { 2825 if (isListShellWithRefresh()) return 'list_submit_only'; 2826 } catch (_eListShell) {} 2827 try { 2828 if (window.isDemandsPage === true) return 'demand_list_submit'; 2829 } catch (_eDemand) {} 2830 return 'external_submit'; 2831 } 2832 2833 function getBodyLocationPageKind() { 2834 try { 2835 if (!document.body || !document.body.classList) return ''; 2836 if (document.body.classList.contains('page-home')) return 'home'; 2837 if (document.body.classList.contains('page-list')) return 'list'; 2838 if (document.body.classList.contains('page-preview')) return 'preview'; 2839 if (document.body.classList.contains('page-demands')) return 'demands'; 2840 } catch (_e) {} 2841 return ''; 2842 } 2843 2844 function getLegacyMarkupPageKind() { 2845 try { 2846 var LEG = window.MirelaLocationLegacyOrchestration; 2847 return LEG && typeof LEG.resolvePageKind === 'function' 2848 ? String(LEG.resolvePageKind() || '').trim().toLowerCase() 2849 : ''; 2850 } catch (_e) { 2851 return ''; 2852 } 2853 } 2854 2855 /** 2856 * Single authoritative page kind for location/search chrome (SSR shell first, then markup, then runtime probes). 2857 * @returns {'home'|'list'|'preview'|'demands'|'other'} 2858 */ 2859 function resolveLocationPageKind() { 2860 var bodyPk = getBodyLocationPageKind();
2861 if (bodyPk) return bodyPk; 2862 2863 var legPk = getLegacyMarkupPageKind(); 2864 if (legPk === 'preview' || legPk === 'demands' || legPk === 'list' || legPk === 'home') return legPk; 2865 2866 try { 2867 var shell = getSearchShell(); 2868 var spk = String(shell.pageKind || '').trim().toLowerCase(); 2869 if (spk === 'preview' || spk === 'demands' || spk === 'list' || spk === 'home') return spk; 2870 if (shell.isPreview === true) return 'preview'; 2871 if (shell.isDemands === true) return 'demands'; 2872 if (shell.isList === true) return 'list'; 2873 if (shell.isHome === true) return 'home'; 2874 } catch (_eShell) {} 2875 2876 try { 2877 if (isOfferPreviewBlockingListContext()) return 'preview'; 2878 } catch (_ePv) {} 2879 2880 try { 2881 if (window.isDemandsPage === true) return 'demands'; 2882 } catch (_eDm) {} 2883 2884 try { 2885 if (isListSearchContext()) return 'list'; 2886 } catch (_eLi) {} 2887 2888 try { 2889 if (isHomePage()) return 'home'; 2890 } catch (_eHm) {} 2891 2892 return 'other'; 2893 } 2894 2895 function isHomeOnlyBrowse() { 2896 try { 2897 return !!isHomePage() && !isListSearchContext(); 2898 } catch (_e) { 2899 return false; 2900 } 2901 } 2902 2903 var api = root.pageContext = root.pageContext || {}; 2904 2905 api.getSearchShell = getSearchShell; 2906 api.hasServerShell = hasServerShell; 2907 api.isListRoutePath = isListRoutePath; 2908 api.detectLocale = detectLocale; 2909 api.isOfferPreviewBlockingListContext = isOfferPreviewBlockingListContext; 2910 api.isListSearchContext = isListSearchContext; 2911 api.isListSearchContextEarly = isListSearchContextEarly; 2912 api.isListAjaxAvailable = isListAjaxAvailable; 2913 api.isListShellWithRefresh = isListShellWithRefresh; 2914 api.isHomePage = isHomePage; 2915 api.isListMapPage = isListMapPage; 2916 api.isOfferLikeListShapedPath = isOfferLikeListShapedPath; 2917 api.getSearchProfileName = getSearchProfileName; 2918 2919 var pageKindApi = root.pageKind = root.pageKind || {}; 2920 pageKindApi.resolve = resolveLocationPageKind; 2921 pageKindApi.isHomeOnlyBrowse = isHomeOnlyBrowse; 2922})(window, document); 2923 2924// ---- END: js/components/search-page-context.js ---- 2925 2926// ---- START: js/components/search-runtime-state.js ---- 2927/* Central runtime state for shared search/list/home flags. */ 2928(function (window, document) { 2929 'use strict'; 2930 2931 var root = window.MirelaSearch = window.MirelaSearch || {}; 2932 var api = root.runtimeState = root.runtimeState || {}; 2933 var state = api.__state || (api.__state = {}); 2934 2935 function hasOwn(obj, key) { 2936 return Object.prototype.hasOwnProperty.call(obj, key); 2937 } 2938 2939 function emitChange(key, value, previousValue) { 2940 try { 2941 document.dispatchEvent(new CustomEvent('mirela:runtime-state-change', { 2942 detail: { 2943 key: String(key || ''), 2944 value: value, 2945 previousValue: previousValue 2946 } 2947 })); 2948 } catch (_e) {} 2949 } 2950 2951 function get(key, fallback) { 2952 var name = String(key || ''); 2953 if (!name) return fallback; 2954 if (hasOwn(state, name)) return state[name]; 2955 if (hasOwn(window, name)) return window[name]; 2956 return fallback; 2957 } 2958 2959 function set(key, value, opts) { 2960 var name = String(key || ''); 2961 if (!name) return value; 2962 var options = opts || {}; 2963 var previousValue = get(name); 2964 state[name] = value; 2965 if (options.mirrorWindow !== false) { 2966 try { window[name] = value; } catch (_e) {} 2967 } 2968 emitChange(name, value, previousValue); 2969 return value; 2970 } 2971 2972 function getFlag(key) { 2973 return get(key) === true; 2974 } 2975 2976 function setFlag(key, enabled, opts) { 2977 return set(key, enabled === true, opts); 2978 } 2979 2980 function clear(key, opts) { 2981 var name = String(key || ''); 2982 if (!name) return false; 2983 var options = opts || {}; 2984 var previousValue = get(name); 2985 try { delete state[name]; } catch (_e) { state[name] = undefined; } 2986 if (options.clearWindow !== false) { 2987 try { delete window[name]; }
2987 catch (_e2) { try { window[name] = undefined; } catch (_e3) {} } 2988 } 2989 emitChange(name, undefined, previousValue); 2990 return true; 2991 } 2992 2993 function snapshot() { 2994 var out = {}; 2995 try { 2996 Object.keys(state).forEach(function (key) { 2997 out[key] = state[key]; 2998 }); 2999 } catch (_e) {} 3000 return out; 3001 } 3002 3003 api.get = get; 3004 api.set = set; 3005 api.getFlag = getFlag; 3006 api.setFlag = setFlag; 3007 api.clear = clear; 3008 api.snapshot = snapshot; 3009})(window, document); 3010 3011// ---- END: js/components/search-runtime-state.js ---- 3012 3013// ---- START: js/components/search-debug.js ---- 3014/* Development-only debug logger for shared search flow. */ 3015(function (window) { 3016 'use strict'; 3017 3018 var root = window.MirelaSearch = window.MirelaSearch || {}; 3019 var api = root.debug = root.debug || {}; 3020 3021 function isEnabled() { 3022 try { 3023 if (window.__MIRELA_SEARCH_DEBUG__ === true) return true; 3024 } catch (_e) {} 3025 try { 3026 var host = String(window.location.hostname || '').toLowerCase(); 3027 return host === 'localhost' || 3028 host === '127.0.0.1' || 3029 /\.test$/.test(host) || 3030 /\.local$/.test(host); 3031 } catch (_e2) { 3032 return false; 3033 } 3034 } 3035 3036 function log(scope, message, payload) { 3037 if (!isEnabled()) return; 3038 try { 3039 if (payload === undefined) { 3040 console.debug('[MirelaSearch][' + String(scope || 'general') + '] ' + String(message || '')); 3041 return; 3042 } 3043 console.debug('[MirelaSearch][' + String(scope || 'general') + '] ' + String(message || ''), payload); 3044 } catch (_e) {} 3045 } 3046 3047 function warn(scope, message, errOrPayload) { 3048 if (!isEnabled()) return; 3049 try { 3050 console.warn('[MirelaSearch][' + String(scope || 'general') + '] ' + String(message || ''), errOrPayload); 3051 } catch (_e) {} 3052 } 3053 3054 api.isEnabled = isEnabled; 3055 api.log = log; 3056 api.warn = warn; 3057})(window); 3058 3059// ---- END: js/components/search-debug.js ---- 3060 3061// ---- START: js/components/search-agent-path-utils.js ---- 3062/* Shared helpers for SEO agent listing paths -> canonical normal list paths. */ 3063(function (window) { 3064 'use strict'; 3065 3066 var root = window.MirelaSearch = window.MirelaSearch || {}; 3067 var api = root.agentPathUtils = root.agentPathUtils || {}; 3068 3069 function inferDealTypeFromAgentListingPath(pathname) { 3070 try { 3071 var p = String(pathname || '').toLowerCase(); 3072 if (p.indexOf('imoti-pod-naem-na-agent-') !== -1 || p.indexOf('properties-for-rent-by-agent-') !== -1 || p.indexOf('immobilien-zur-miete-von-agent-') !== -1 || p.indexOf('nedvizhimost-v-arendu-u-agenta-') !== -1) return '2'; 3073 if (p.indexOf('imoti-za-prodazhba-na-agent-') !== -1 || p.indexOf('properties-for-sale-by-agent-') !== -1 || p.indexOf('immobilien-zum-verkauf-von-agent-') !== -1 || p.indexOf('nedvizhimost-na-prodazhu-u-agenta-') !== -1) return '1'; 3074 if (p.indexOf('zayavki-ot-kupuvachi-na-agent-') !== -1 || p.indexOf('buyer-requests-by-agent-') !== -1 || p.indexOf('kaufanfragen-bei-agent-') !== -1 || p.indexOf('zayavki-ot-pokupateley-u-agenta-') !== -1) return '3'; 3075 if (p.indexOf('zayavki-ot-naemateli-na-agent-') !== -1 || p.indexOf('tenant-requests-by-agent-') !== -1 || p.indexOf('mietanfragen-bei-agent-') !== -1 || p.indexOf('zayavki-ot-arendatorov-u-agenta-') !== -1) return '4'; 3076 } catch (_e) {} 3077 return ''; 3078 } 3079 3080 function detectLocale(pathname) { 3081 try { 3082 var route = root.routeUtils || null; 3083 if (route && typeof route.detectLocaleFromPath === 'function') { 3084 return String(route.detectLocaleFromPath(pathname || '') || 'bg').toLowerCase(); 3085 } 3086 } catch (_e) {} 3087 return 'bg'; 3088 } 3089 3090 function getDealSegment(locale, typeValue) { 3091 try { 3092 var builder = root.listUrlBuilder || null; 3093 if (builder && typeof builder.getDealSegment === 'function') { 3094 return String(builder.getDealSegment(locale, typeValue) || '').trim(); 3095 } 3096 } catch (_e) {} 3097 var map = { 3098 bg: { '1': 'prodazhbi', '2': 'naemi', '3': 'kupuvachi', '4': 'naemateli' }, 3099 en: { '1': 'for-sale', '2': 'for-rent', '3': 'kupuvachi', '4': 'naemateli' }, 3100 ru: { '1': 'prodazha', '2': 'arenda', '3': 'kupuvachi', '4': 'naemateli' }, 3101 de: { '1': 'verkauf', '2': 'miete', '3': 'kupuvachi', '4': 'naemateli' } 3102 }; 3103 var l = String(locale || 'bg').toLowerCase(); 3104 var t = String(typeValue || '1'); 3105 return (map[l] && map[l][t]) || map.bg[t] || 'prodazhbi'; 3106 } 3107 3108 function buildCanonicalListPathFromAgentPath(pathname, explicitType) { 3109 try { 3110 var locale = detectLocale(pathname || ''); 3111 var typeValue = String(explicitType || inferDealTypeFromAgentListingPath(pathname || '') || '').trim(); 3112 if (!/^[1-4]$/.test(typeValue)) return ''; 3113 var segment = getDealSegment(locale, typeValue); 3114 if (!segment) return ''; 3115 return (locale === 'bg' ? '' : ('/' + locale)) + '/' + segment; 3116 } catch (_e) { 3117 return ''; 3118 } 3119 } 3120 3121 var AGENT_LISTING_PREFIX_MAP = { 3122 bg: { 3123 '1': 'imoti-za-prodazhba-na-agent', 3124 '2': 'imoti-pod-naem-na-agent', 3125 '3': 'zayavki-ot-kupuvachi-na-agent', 3126 '4': 'zayavki-ot-naemateli-na-agent' 3127 }, 3128 en: { 3129 '1': 'properties-for-sale-by-agent', 3130 '2': 'properties-for-rent-by-agent', 3131 '3': 'buyer-requests-by-agent', 3132 '4': 'tenant-requests-by-agent' 3133 }, 3134 ru: { 3135 '1': 'nedvizhimost-na-prodazhu-u-agenta', 3136 '2': 'nedvizhimost-v-arendu-u-agenta', 3137 '3': 'zayavki-ot-pokupateley-u-agenta', 3138 '4': 'zayavki-ot-arendatorov-u-agenta' 3139 }, 3140 de: { 3141 '1': 'immobilien-zum-verkauf-von-agent', 3142 '2': 'immobilien-zur-miete-von-agent', 3143 '3': 'kaufanfragen-bei-agent', 3144 '4': 'mietanfragen-bei-agent' 3145 } 3146 }; 3147 3148 // Given the current agent SEO path, return the agent path for another deal type 3149 // (e.g. /imoti-za-prodazhba-na-agent-⦠â /imoti-pod-naem-na-agent-â¦). Empty if not applicable. 3150 function buildAgentListingPathForType(pathname, typeNum) { 3151 try { 3152 if (!isAgentListingPath(pathname)) return ''; 3153 var pathOnly = String(pathname || '').split('?')[0] || ''; 3154 var locale = detectLocale(pathname || '');
3155 var parts = pathOnly.split('/').filter(Boolean); 3156 if (!parts.length) return ''; 3157 var idx = locale === 'bg' ? 0 : 1; 3158 if (idx >= parts.length) return ''; 3159 var currentSegment = String(parts[idx] || '').trim(); 3160 if (!currentSegment) return ''; 3161 var suffix = ''; 3162 Object.keys(AGENT_LISTING_PREFIX_MAP).some(function (mapLocale) { 3163 var localeMap = AGENT_LISTING_PREFIX_MAP[mapLocale] || {}; 3164 return Object.keys(localeMap).some(function (mapType) { 3165 var prefix = String(localeMap[mapType] || '').trim(); 3166 var token = prefix ? (prefix + '-') : ''; 3167 if (!token || currentSegment.indexOf(token) !== 0) return false; 3168 suffix = currentSegment.slice(prefix.length); 3169 return true; 3170 }); 3171 }); 3172 var targetMap = AGENT_LISTING_PREFIX_MAP[locale] || AGENT_LISTING_PREFIX_MAP.bg; 3173 var nextPrefix = String(targetMap[String(typeNum || '').trim()] || '').trim(); 3174 if (!suffix || !nextPrefix) return ''; 3175 parts[idx] = nextPrefix + suffix; 3176 return '/' + parts.join('/'); 3177 } catch (_e) { 3178 return ''; 3179 } 3180 } 3181 3182 function isAgentListingPath(pathname) { 3183 try { 3184 var pathOnly = String(pathname || '').split('?')[0] || ''; 3185 var segments = pathOnly.split('/').filter(Boolean); 3186 if (!segments.length) return false; 3187 var idx = 0; 3188 var first = String(segments[0] || '').toLowerCase(); 3189 if (first === 'bg' || first === 'en' || first === 'de' || first === 'ru') { 3190 idx = 1; 3191 } 3192 if (!segments[idx]) return false; 3193 var slugSegment = String(segments[idx] || ''); 3194 if (!/-\d+$/.test(slugSegment)) return false; 3195 var prefixes = [ 3196 'imoti-za-prodazhba-na-agent', 3197 'imoti-pod-naem-na-agent', 3198 'zayavki-ot-kupuvachi-na-agent', 3199 'zayavki-ot-naemateli-na-agent', 3200 'properties-for-sale-by-agent', 3201 'properties-for-rent-by-agent', 3202 'buyer-requests-by-agent', 3203 'tenant-requests-by-agent', 3204 'immobilien-zum-verkauf-von-agent', 3205 'immobilien-zur-miete-von-agent', 3206 'kaufanfragen-bei-agent', 3207 'mietanfragen-bei-agent', 3208 'nedvizhimost-na-prodazhu-u-agenta', 3209 'nedvizhimost-v-arendu-u-agenta', 3210 'zayavki-ot-pokupateley-u-agenta', 3211 'zayavki-ot-arendatorov-u-agenta' 3212 ]; 3213 for (var i = 0; i < prefixes.length; i++) { 3214 if (slugSegment.indexOf(prefixes[i] + '-') === 0) return true; 3215 } 3216 } catch (_e2) {} 3217 return false; 3218 } 3219 3220 api.inferDealTypeFromAgentListingPath = inferDealTypeFromAgentListingPath; 3221 api.buildCanonicalListPathFromAgentPath = buildCanonicalListPathFromAgentPath; 3222 api.isAgentListingPath = isAgentListingPath; 3223 api.buildAgentListingPathForType = buildAgentListingPathForType; 3224})(window); 3225 3226// ---- END: js/components/search-agent-path-utils.js ---- 3227 3228// ---- START: js/components/advanced-filters-panel-controls.js ---- 3229/* Advanced filters panel open/close controls and safety guards. */ 3230(function (window, document) { 3231 'use strict'; 3232 3233 function removeBackdropsAndModalState() { 3234 try { 3235 document.querySelectorAll('.modal-backdrop, .offcanvas-backdrop').forEach(function (b) { 3236 try { b.parentNode && b.parentNode.removeChild(b); } catch (_e) {} 3237 }); 3238 document.body.classList.remove('modal-open'); 3239 document.body.style.removeProperty('padding-right'); 3240 } catch (_e2) {} 3241 } 3242 3243 function cleanupBlockingOverlays() { 3244 try { 3245 document.querySelectorAll('.modal.show').forEach(function (m) { 3246 try { 3247 if (window.bootstrap && window.bootstrap.Modal) { 3248 var inst = window.bootstrap.Modal.getInstance(m) || window.bootstrap.Modal.getOrCreateInstance(m); 3249 if (inst && inst.hide) inst.hide(); 3250 } 3251 } catch (_e2) {} 3252 try { m.classList.remove('show'); } catch (_eModalClass) {} 3253 }); 3254 document.querySelectorAll('.offcanvas.show').forEach(function (oc) { 3255 try { 3256 if (window.bootstrap && window.bootstrap.Offcanvas) { 3257 var oi = window.bootstrap.Offcanvas.getInstance(oc) || window.bootstrap.Offcanvas.getOrCreateInstance(oc); 3258 if (oi && oi.hide) oi.hide(); 3259 } 3260 } catch (_e3) {} 3261 try { oc.classList.remove('show'); } catch (_e4) {} 3262 }); 3263 } catch (_e5) {} 3264 removeBackdropsAndModalState(); 3265 } 3266 3267 function setHomeOfferTabsZIndex(zIndexValue) { 3268 try { 3269 var tabs = document.querySelector('.home-offer-tabs'); 3270 if (tabs) tabs.style.setProperty('z-index', String(zIndexValue), 'important'); 3271 } catch (_e) {} 3272 } 3273 3274 function setHeaderZIndexForFiltersOpen(isOpen) { 3275 try { 3276 if (window.innerWidth >= 1100) return; 3277 var hdr = document.querySelector('.main-header.fixed-header'); 3278 if (!hdr) return; 3279 if (isOpen) { 3280 hdr.style.setProperty('z-index', '-15', 'important'); 3281 } else { 3282 var st = window.pageYOffset || document.documentElement.scrollTop || 0; 3283 var onHome = document.body && document.body.classList.contains('page-home'); 3284 var z = onHome ? '10100' : (st > 250 ? '9999' : '999'); 3285 hdr.style.setProperty('z-index', z, 'important'); 3286 } 3287 } catch (_e2) {} 3288 } 3289
3290 function setMobileSearchWrapHidden(hidden) { 3291 try { 3292 var inp = document.querySelector('.js-mobile-search-input'); 3293 var wrap = inp && inp.closest('.flex-grow-1'); 3294 if (!wrap) return; 3295 if (hidden) wrap.style.setProperty('display', 'none', 'important'); 3296 else wrap.style.removeProperty('display'); 3297 } catch (_e) {} 3298 } 3299 3300 function syncFiltersOpenClass(show) { 3301 try { 3302 if (window.innerWidth < 1100) { 3303 if (show) document.body.classList.add('filters-open'); 3304 else document.body.classList.remove('filters-open'); 3305 } 3306 } catch (_e) {} 3307 } 3308 3309 function isAdvancedPanelOpen() { 3310 try { 3311 var form = document.querySelector('.wd-search-form'); 3312 return !!(form && form.classList.contains('show')); 3313 } catch (_e) { 3314 return false; 3315 } 3316 } 3317 3318 function toggleAdvFilters(show) { 3319 const form = document.querySelector('.wd-search-form'); 3320 if (!form) return; 3321 3322 if (show) { 3323 cleanupBlockingOverlays(); 3324 3325 form.classList.add('show'); 3326 setHomeOfferTabsZIndex(1000); 3327 setHeaderZIndexForFiltersOpen(true); 3328 } else { 3329 form.classList.remove('show'); 3330 setHomeOfferTabsZIndex(100003); 3331 setHeaderZIndexForFiltersOpen(false); 3332 } 3333 3334 syncFiltersOpenClass(show); 3335 } 3336 3337 function attachAdvancedToggleListeners() { 3338 if (document.__mirelaAdvancedToggleBound) return; 3339 document.__mirelaAdvancedToggleBound = true; 3340 3341 document.addEventListener('click', function (e) { 3342 var target = e && e.target && e.target.closest 3343 ? e.target.closest('.filter-advanced, #toggleAdvFilters, .js-mobile-filter-btn') 3344 : null; 3345 if (!target) return; 3346 if (e.target && e.target.closest && e.target.closest('.filter-advanced-clear, [aria-label="ÐзÑиÑÑи"]')) { 3347 return; 3348 } 3349 try { e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); } catch (_) {} 3350 3351 var shouldOpen = !isAdvancedPanelOpen(); 3352 if (window.MobileFilters && typeof window.MobileFilters.open === 'function' && typeof window.MobileFilters.close === 'function') { 3353 if (shouldOpen) window.MobileFilters.open(); 3354 else window.MobileFilters.close(); 3355 return; 3356 } 3357 toggleAdvFilters(shouldOpen); 3358 }, true); 3359 3360 var closeBtn = document.getElementById('js-mobile-close-filters'); 3361 if (closeBtn) { 3362 if (closeBtn.__mirela_mobile_close_filters_bound) { 3363 closeBtn.removeEventListener('click', closeBtn.__mirela_mobile_close_filters_bound); 3364 } 3365 closeBtn.__mirela_mobile_close_filters_bound = function () { 3366 if (window.MobileFilters && typeof window.MobileFilters.close === 'function') { 3367 window.MobileFilters.close(); 3368 } else { 3369 toggleAdvFilters(false); 3370 } 3371 }; 3372 closeBtn.addEventListener('click', closeBtn.__mirela_mobile_close_filters_bound); 3373 } 3374 } 3375 3376 function restoreUiAfterClose() { 3377 try { 3378 setHomeOfferTabsZIndex(100003); 3379 syncFiltersOpenClass(false); 3380 removeBackdropsAndModalState(); 3381 setHeaderZIndexForFiltersOpen(false); 3382 setMobileSearchWrapHidden(false); 3383 try { 3384 document.querySelectorAll('.wrap-filter-search').forEach(function (el) { 3385 el.removeAttribute('style'); 3386 }); 3387 } catch (_e3) {} 3388 } catch (_e4) {} 3389 } 3390 3391 function applyUiOnOpen() { 3392 try { 3393 setHomeOfferTabsZIndex(1000); 3394 setHeaderZIndexForFiltersOpen(true); 3395 syncFiltersOpenClass(true);
3396 setMobileSearchWrapHidden(true); 3397 try { document.dispatchEvent(new CustomEvent('filters-panel-opened')); } catch (_e2) {} 3398 } catch (_e3) {} 3399 } 3400 3401 function initObserver() { 3402 try { 3403 var panel = document.querySelector('.wd-search-form'); 3404 if (!panel) return; 3405 var obs = new MutationObserver(function (muts) { 3406 muts.forEach(function (m) { 3407 if (m.attributeName === 'class') { 3408 var shown = panel.classList.contains('show'); 3409 if (shown) applyUiOnOpen(); 3410 else restoreUiAfterClose(); 3411 } 3412 }); 3413 }); 3414 obs.observe(panel, { attributes: true, attributeFilter: ['class'] }); 3415 } catch (_e) {} 3416 } 3417 3418 function init() { 3419 attachAdvancedToggleListeners(); 3420 try { 3421 window.addEventListener('pageshow', function () { 3422 setTimeout(function () { 3423 restoreUiAfterClose(); 3424 }, 0); 3425 }); 3426 } catch (_ePs) {} 3427 initObserver(); 3428 } 3429 3430 window.toggleAdvFilters = toggleAdvFilters; 3431 window.attachAdvancedToggleListeners = attachAdvancedToggleListeners; 3432 3433 if (document.readyState === 'loading') { 3434 document.addEventListener('DOMContentLoaded', init); 3435 } else { 3436 init(); 3437 } 3438 3439 var ms = window.MirelaSearch = window.MirelaSearch || {}; 3440 ms.uiShell = ms.uiShell || {}; 3441 ms.uiShell.removeBackdropsAndModalState = removeBackdropsAndModalState; 3442 ms.uiShell.cleanupBlockingOverlays = cleanupBlockingOverlays; 3443})(window, document); 3444 3445 3446// ---- END: js/components/advanced-filters-panel-controls.js ---- 3447 3448// ---- START: js/components/district-polygons-map-apply.js ---- 3449/** 3450 * Shared: apply a /address or /api/locations/polygons style response onto MapLeaflet. 3451 * - preserveMarkers: true â setDistrictPolygons when available (list SEO hydrate, home restore) so pins are not wiped. 3452 * - preserveMarkers: false â MapLeaflet.render({ data: [], ... }) first (home autocomplete / mobile flow). 3453 */ 3454(function (window) { 3455 'use strict'; 3456 3457 var root = (window.MirelaSearch = window.MirelaSearch || {}); 3458 3459 /** 3460 * @param {object} ml MapLeaflet API 3461 * @param {{ district_polygons?: object, bounds?: object }} resp 3462 * @param {{ preserveMarkers?: boolean, markerData?: array }} [opts] 3463 * @returns {boolean} true if polygons were applied 3464 */ 3465 root.applyDistrictPolygonsToMapLeaflet = function (ml, resp, opts) { 3466 opts = opts || {}; 3467 if (!ml || !resp) { 3468 return false; 3469 } 3470 var polys = resp.district_polygons; 3471 if (!polys || typeof polys !== 'object') { 3472 return false; 3473 } 3474 var keys = Object.keys(polys); 3475 if (!keys.length) { 3476 return false; 3477 } 3478 var preserve = opts.preserveMarkers === true; 3479 var markerData = opts.markerData != null ? opts.markerData : []; 3480 try { 3481 if (preserve && typeof ml.setDistrictPolygons === 'function') { 3482 ml.setDistrictPolygons(polys); 3483 return true; 3484 } 3485 if (typeof ml.render === 'function') { 3486 ml.render({ 3487 data: markerData, 3488 district_polygons: polys, 3489 bounds: resp.bounds, 3490 }); 3491 return true; 3492 } 3493 if (typeof ml.setDistrictPolygons === 'function') { 3494 ml.setDistrictPolygons(polys); 3495 return true; 3496 } 3497 } catch (_e) {} 3498 return false; 3499 }; 3500 3501 root.hasNonEmptyDistrictPolygons = function (resp) { 3502 if (!resp || !resp.district_polygons || typeof resp.district_polygons !== 'object') { 3503 return false; 3504 } 3505 return Object.keys(resp.district_polygons).length > 0; 3506 }; 3507 3508 root.clearDistrictPolygonsOnMap = function (ml) { 3509 try { 3510 if (ml && typeof ml.setDistrictPolygons === 'function') { 3511 ml.setDistrictPolygons({}); 3512 } 3513 } catch (_e) {} 3514 }; 3515 3516 /** 3517 * @param {string[]|string|null|undefined} districtIds array or CSV 3518 * @param {string[]|string|null|undefined} groupIds array or CSV 3519 * @returns {Promise<object>} parsed JSON from /api/locations/polygons 3520 */ 3521 root.fetchDistrictPolygonsFromApi = function (districtIds, groupIds) { 3522 var usp = new URLSearchParams(); 3523 var dStr = 3524 districtIds != null && districtIds !== '' 3525 ? Array.isArray(districtIds) 3526 ? districtIds 3527 .map(function (v) { 3528 return String(v || '').trim(); 3529 }) 3530 .filter(Boolean) 3531 .join(',') 3532 : String(districtIds).trim() 3533 : ''; 3534 var gStr = 3535 groupIds != null && groupIds !== '' 3536 ? Array.isArray(groupIds) 3537 ? groupIds 3538 .map(function (v) { 3539 return String(v || '').trim(); 3540 }) 3541 .filter(Boolean) 3542 .join(',') 3543 : String(groupIds).trim() 3544 : ''; 3545 if (dStr) { 3546 usp.set('district_ids', dStr); 3547 } 3548 if (gStr) { 3549 usp.set('group_ids', gStr); 3550 } 3551 var q = usp.toString(); 3552 if (!q) { 3553 return Promise.resolve({ district_polygons: {}, bounds: null });
3554 } 3555 return fetch('/api/locations/polygons?' + q, { credentials: 'same-origin' }).then(function (r) { 3556 return r.json(); 3557 }); 3558 }; 3559})(window); 3560 3561// ---- END: js/components/district-polygons-map-apply.js ---- 3562 3563// ---- START: js/components/home/map-user-gesture-tracker.js ---- 3564/* Home map user gesture tracker. */ 3565(function (window, document) { 3566 'use strict'; 3567 3568 var root = window.MirelaSearch = window.MirelaSearch || {}; 3569 var api = root.homeMapUserGestureTracker = root.homeMapUserGestureTracker || {}; 3570 3571 function isHomeOffersCarouselGestureBlocked() { 3572 try { 3573 return window.__mirelaHomeOffersCarouselGesture === true; 3574 } catch (_error) { 3575 return false; 3576 } 3577 } 3578 3579 /** 3580 * Reuse the shared runtime flag API when available, otherwise fall back to 3581 * the legacy globals still consumed by MapLeaflet and home.js. 3582 */ 3583 function resolveListRuntimeFlags() { 3584 try { 3585 var mirelaSearch = window.MirelaSearch; 3586 if (!mirelaSearch) return null; 3587 if (mirelaSearch.listRuntimeFlags) return mirelaSearch.listRuntimeFlags; 3588 if (mirelaSearch.listPageRuntimeFlags && typeof mirelaSearch.listPageRuntimeFlags.create === 'function') { 3589 return mirelaSearch.listPageRuntimeFlags.create(); 3590 } 3591 } catch (_error) {} 3592 return null; 3593 } 3594 3595 /** 3596 * Mark the map as user-driven after a real drag/zoom gesture. 3597 * The home-specific flag is intentionally set only on the homepage. 3598 */ 3599 function markMapAsUserDriven(isHomePage) { 3600 try { 3601 var runtimeFlags = resolveListRuntimeFlags(); 3602 if (runtimeFlags && typeof runtimeFlags.setUserMovedMapFlag === 'function') { 3603 runtimeFlags.setUserMovedMapFlag(true); 3604 } else { 3605 window.__mirelaUserMovedMap = true; 3606 } 3607 3608 if (isHomePage !== true) return; 3609 3610 if (runtimeFlags && typeof runtimeFlags.setUserMovedMapHomeFlag === 'function') { 3611 runtimeFlags.setUserMovedMapHomeFlag(true); 3612 } else { 3613 window.__mirelaUserMovedMapHome = true; 3614 } 3615 3616 if (runtimeFlags && typeof runtimeFlags.mirrorNow === 'function') { 3617 runtimeFlags.mirrorNow(); 3618 } 3619 } catch (_error) {} 3620 } 3621 3622 /** 3623 * Attach a minimal set of listeners that detect real user interaction with 3624 * the home map. Programmatic moves should not set the flags. 3625 */ 3626 function create(options) { 3627 options = options || {}; 3628 3629 function isHomePage() { 3630 try { 3631 return typeof options.isHomePage === 'function' && options.isHomePage() === true; 3632 } catch (_error) { 3633 return false; 3634 } 3635 } 3636 3637 function getLeafletMap() { 3638 try { 3639 return (typeof options.getLeafletMap === 'function') ? options.getLeafletMap() : null; 3640 } catch (_error) { 3641 return null; 3642 } 3643 } 3644 3645 function markFromGesture() { 3646 if (isHomeOffersCarouselGestureBlocked()) return; 3647 markMapAsUserDriven(isHomePage()); 3648 } 3649 3650 function attachTouchGestureFallback(leafletMap) { 3651 try { 3652 var container = leafletMap && typeof leafletMap.getContainer === 'function' 3653 ? leafletMap.getContainer() 3654 : null; 3655 if (!container || container.__mirelaHomeTouchGestureFallbackAttached === true) { 3656 return; 3657 } 3658 container.__mirelaHomeTouchGestureFallbackAttached = true; 3659 3660 container.addEventListener('touchstart', function (event) { 3661 try { 3662 if (!event || !event.touches || event.touches.length < 2) return; 3663 markFromGesture(); 3664 } catch (_touchStartError) {} 3665 }, { passive: true, capture: true });
3666 } catch (_error) {} 3667 } 3668 3669 function attachZoomControlListener() { 3670 try { 3671 var zoomControls = document.querySelector('.leaflet-control-zoom'); 3672 if (!zoomControls) { 3673 setTimeout(attachZoomControlListener, 100); 3674 return; 3675 } 3676 zoomControls.addEventListener('click', function (event) { 3677 var target = event.target; 3678 if (!target) return; 3679 if ( 3680 target.classList.contains('leaflet-control-zoom-in') || 3681 target.classList.contains('leaflet-control-zoom-out') || 3682 target.closest('.leaflet-control-zoom-in') || 3683 target.closest('.leaflet-control-zoom-out') 3684 ) { 3685 markFromGesture(); 3686 } 3687 }, true); 3688 } catch (_error) {} 3689 } 3690 3691 /** 3692 * Start listening once the Leaflet instance exists. 3693 */ 3694 function startTracking() { 3695 try { 3696 if (window.__mirelaHomeMapUserGestureTrackerStarted) return; 3697 window.__mirelaHomeMapUserGestureTrackerStarted = true; 3698 3699 var attachWhenReady = function () { 3700 try { 3701 var leafletMap = getLeafletMap(); 3702 if (!leafletMap || !leafletMap.on) { 3703 setTimeout(attachWhenReady, 250); 3704 return; 3705 } 3706 3707 leafletMap.on('dragstart', function () { 3708 markFromGesture(); 3709 }); 3710 3711 leafletMap.on('movestart', function (event) { 3712 if (event && event.originalEvent) { 3713 markFromGesture(); 3714 } 3715 }); 3716 3717 leafletMap.on('zoomstart', function (event) { 3718 if (event && event.originalEvent) { 3719 markFromGesture(); 3720 } 3721 }); 3722 3723 leafletMap.on('zoomend', function (event) { 3724 if (event && event.originalEvent) { 3725 markFromGesture(); 3726 } 3727 }); 3728 3729 attachTouchGestureFallback(leafletMap); 3730 attachZoomControlListener(); 3731 } catch (_error) {} 3732 }; 3733 3734 setTimeout(attachWhenReady, 0); 3735 } catch (_error) {} 3736 } 3737 3738 return { 3739 startTracking: startTracking 3740 }; 3741 } 3742 3743 api.create = create; 3744})(window, document); 3745 3746// ---- END: js/components/home/map-user-gesture-tracker.js ---- 3747 3748// ---- START: js/components/search-canonical-rel-parser.js ---- 3749/* Shared parser for canonical list relative URLs -> search/filter state fragments. */ 3750(function (window) { 3751 'use strict'; 3752 3753 var root = window.MirelaSearch = window.MirelaSearch || {}; 3754 var api = root.searchCanonicalRelParser = root.searchCanonicalRelParser || {}; 3755 3756 function trim(value) { 3757 return String(value == null ? '' : value).trim(); 3758 } 3759 3760 function parseQuery(urlOrQuery) { 3761 try { 3762 var q = urlOrQuery || ''; 3763 if (q && q[0] === '/' && q.indexOf('?') === -1) return {}; 3764 if (q.indexOf('?') !== -1) q = q.split('?')[1] || ''; 3765 var usp = new URLSearchParams(q); 3766 var out = {};
3767 usp.forEach(function (v, k) { 3768 var key = String(k || ''); 3769 if (key.slice(-2) === '[]') { 3770 var arrKey = key.slice(0, -2); 3771 if (!Array.isArray(out[arrKey])) out[arrKey] = []; 3772 out[arrKey].push(v); 3773 return; 3774 } 3775 if (Object.prototype.hasOwnProperty.call(out, key)) { 3776 if (!Array.isArray(out[key])) out[key] = [out[key]]; 3777 out[key].push(v); 3778 return; 3779 } 3780 out[key] = v; 3781 }); 3782 return out; 3783 } catch (_e) { 3784 return {}; 3785 } 3786 } 3787 3788 function getTailConfig() { 3789 try { 3790 return window.__mirelaSearchBarTailConfig || {}; 3791 } catch (_e) { 3792 return {}; 3793 } 3794 } 3795 3796 function getTopTypesGroups() { 3797 try { 3798 if (window.__mirelaTopTypesConfig && Array.isArray(window.__mirelaTopTypesConfig.groups)) { 3799 return window.__mirelaTopTypesConfig.groups; 3800 } 3801 } catch (_e) {} 3802 var cfg = getTailConfig(); 3803 return Array.isArray(cfg.topTypesGroups) ? cfg.topTypesGroups : []; 3804 } 3805 3806 function reverseLookupIdBySlug(slugMap, slug, locale) { 3807 try { 3808 var loc = trim(locale || 'bg').toLowerCase() || 'bg'; 3809 var wanted = trim(slug).toLowerCase(); 3810 if (!wanted) return ''; 3811 var byId = (slugMap && (slugMap[loc] || slugMap.bg)) || {}; 3812 var ids = Object.keys(byId); 3813 for (var i = 0; i < ids.length; i++) { 3814 var id = trim(ids[i]); 3815 if (!id) continue; 3816 if (trim(byId[id]).toLowerCase() === wanted) return id; 3817 } 3818 } catch (_e) {} 3819 return ''; 3820 } 3821 3822 /** District group slug: try path locale first, then other locales (explicit slug may exist only under bg). */ 3823 function reverseLookupDistrictGroupIdAllLocales(slugMap, slug, preferredLocale) { 3824 try { 3825 var wanted = trim(String(slug || '')).toLowerCase(); 3826 if (!wanted || !slugMap) return ''; 3827 var order = []; 3828 var pl = trim(preferredLocale || 'bg').toLowerCase() || 'bg'; 3829 order.push(pl); 3830 ['bg', 'en', 'ru', 'de'].forEach(function (L) { 3831 if (order.indexOf(L) === -1) order.push(L); 3832 }); 3833 for (var oi = 0; oi < order.length; oi++) { 3834 var loc = order[oi]; 3835 var byId = (slugMap[loc] || slugMap.bg) || {}; 3836 var ids = Object.keys(byId).map(function (k) { return trim(k); }).filter(Boolean); 3837 ids.sort(function (a, b) { 3838 var na = parseInt(a, 10); var nb = parseInt(b, 10); 3839 if (na !== nb) return na - nb; 3840 return String(a).localeCompare(String(b)); 3841 }); 3842 for (var ii = 0; ii < ids.length; ii++) { 3843 if (trim(String(byId[ids[ii]] || '')).toLowerCase() === wanted) return ids[ii]; 3844 } 3845 } 3846 } catch (_e2) {} 3847 return ''; 3848 } 3849 3850 /** Compound segment: merge district slug tokens from all locales (path often uses BG tokens on /prodazhbi/...). */ 3851 function tryResolveDistrictGroupFromCompoundSeg(seg2, cityId, preferredLocale) { 3852 var cfg = getTailConfig(); 3853 var cid = trim(String(cityId || '')); 3854 var wanted = trim(String(seg2 || '')).toLowerCase(); 3855 if (!cid || !wanted) return ''; 3856 3857 var dgByDist = cfg.districtGroupIdByDistrictId || {}; 3858 var membersByGroup = cfg.districtGroupMemberIdsByGroupId || {}; 3859 var distCity = cfg.districtCityIdById || {}; 3860 var localeOrder = []; 3861 var pl = trim(preferredLocale || 'bg').toLowerCase() || 'bg'; 3862 localeOrder.push(pl); 3863 ['bg', 'en', 'ru', 'de'].forEach(function (L) { 3864 if (localeOrder.indexOf(L) === -1) localeOrder.push(L); 3865 }); 3866 3867 var slugToId = {}; 3868 var ambiguous = false; 3869 localeOrder.forEach(function (loc) { 3870 var slugMap = (cfg.districtSlugById && (cfg.districtSlugById[loc] || cfg.districtSlugById.bg)) || {};
3871 Object.keys(slugMap).forEach(function (idStr) { 3872 if (String(distCity[idStr] || '') !== cid) return; 3873 if (!dgByDist[idStr]) return; 3874 var sl = trim(String(slugMap[idStr] || '')).toLowerCase(); 3875 if (!sl) return; 3876 if (Object.prototype.hasOwnProperty.call(slugToId, sl) && String(slugToId[sl]) !== String(idStr)) { 3877 ambiguous = true; 3878 return; 3879 } 3880 if (!Object.prototype.hasOwnProperty.call(slugToId, sl)) { 3881 slugToId[sl] = idStr; 3882 } 3883 }); 3884 }); 3885 if (ambiguous) return ''; 3886 3887 var slugsByLength = Object.keys(slugToId); 3888 slugsByLength.sort(function (a, b) { return b.length - a.length; }); 3889 3890 var segment = wanted; 3891 var n = segment.length; 3892 var pos = 0; 3893 var matchedIdStrs = []; 3894 while (pos < n) { 3895 if (segment.charAt(pos) === '-') { 3896 pos++; 3897 continue; 3898 } 3899 var found = null; 3900 for (var si = 0; si < slugsByLength.length; si++) { 3901 var candSlug = slugsByLength[si]; 3902 var len = candSlug.length; 3903 if (!len || pos + len > n) continue; 3904 if (segment.slice(pos, pos + len) !== candSlug) continue; 3905 var next = pos + len; 3906 if (next !== n && segment.charAt(next) !== '-') continue; 3907 found = candSlug; 3908 break; 3909 } 3910 if (!found) return ''; 3911 matchedIdStrs.push(slugToId[found]); 3912 pos += found.length; 3913 } 3914 3915 var uniq = []; 3916 matchedIdStrs.forEach(function (x) { 3917 var t = trim(String(x)); 3918 if (t && uniq.indexOf(t) === -1) uniq.push(t); 3919 }); 3920 if (uniq.length < 2) return ''; 3921 3922 var g0 = String(dgByDist[uniq[0]] || ''); 3923 if (!g0) return ''; 3924 for (var ui = 1; ui < uniq.length; ui++) { 3925 if (String(dgByDist[uniq[ui]] || '') !== g0) return ''; 3926 } 3927 3928 var members = membersByGroup[g0] || []; 3929 var a = uniq.slice().sort(function (x, y) { return parseInt(x, 10) - parseInt(y, 10); }); 3930 var b = members.slice().sort(function (x, y) { return parseInt(String(x), 10) - parseInt(String(y), 10); }); 3931 if (a.length !== b.length) return ''; 3932 for (var j = 0; j < a.length; j++) { 3933 if (a[j] !== b[j]) return ''; 3934 } 3935 return g0; 3936 } 3937 3938 function extractStreetIdFromSlug(slug) { 3939 try { 3940 var match = trim(slug).match(/-(\d+)$/); 3941 return (match && match[1]) ? String(match[1]) : ''; 3942 } catch (_e) { 3943 return ''; 3944 } 3945 } 3946 3947 function isNbSeoSlug(slug) { 3948 var value = trim(slug).toLowerCase(); 3949 if (!value) return false; 3950 try { 3951 if (typeof window !== 'undefined' && typeof window.__mirelaIsNbSeoCitySlug === 'function') { 3952 return !!window.__mirelaIsNbSeoCitySlug(value); 3953 } 3954 } catch (_e) {} 3955 return value === 'novi-sgradi-v-stroej' 3956 || value === 'new-buildings-under-construction' 3957 || value === 'novye-zdaniya-v-stroitelstve' 3958 || value === 'neubauten-im-bau'; 3959 } 3960 3961 function parseCanonicalListRel(urlLike) { 3962 var base = parseQuery(urlLike); 3963 try { 3964 var rawRel = trim(urlLike); 3965 if (!rawRel || rawRel[0] !== '/') return base; 3966 var u = new URL(rawRel, window.location.origin); 3967 var route = (root && root.routeUtils) ? root.routeUtils : null; 3968 try { 3969 if (route && typeof route.isOfferLikePath === 'function' && route.isOfferLikePath(u.pathname || '')) { 3970 return base; 3971 } 3972 } catch (_eOfferLike) {} 3973 var parts = route && typeof route.getPathSegments === 'function' 3974 ? route.getPathSegments(u.pathname || '')
3975 : String(u.pathname || '').split('/').filter(Boolean); 3976 var hasLocale = false; 3977 try { 3978 hasLocale = !!(parts.length && route && typeof route.isLocaleSegment === 'function' 3979 ? route.isLocaleSegment(parts[0]) 3980 : /^(bg|en|ru|de)$/i.test(String(parts[0] || ''))); 3981 } catch (_eLocale) {} 3982 var locale = hasLocale ? trim(parts[0]).toLowerCase() : 'bg'; 3983 var dealIdx = hasLocale ? 1 : 0; 3984 var dealSeg = trim(parts[dealIdx]).toLowerCase(); 3985 var cfg = getTailConfig(); 3986 var citySlug = trim(parts[dealIdx + 1]); 3987 var seg2 = trim(parts[dealIdx + 2]); 3988 var seg3 = trim(parts[dealIdx + 3]); 3989 var seg4 = trim(parts[dealIdx + 4]); 3990 if (isNbSeoSlug(citySlug) && reverseLookupIdBySlug(cfg.citySlugById, seg2, locale)) { 3991 citySlug = trim(parts[dealIdx + 2]); 3992 seg2 = trim(parts[dealIdx + 3]); 3993 seg3 = trim(parts[dealIdx + 4]); 3994 seg4 = trim(parts[dealIdx + 5]); 3995 base.has_project = base.has_project || '1'; 3996 } 3997 var topGroups = getTopTypesGroups(); 3998 var topSlugToIds = {}; 3999 topGroups.forEach(function (group) { 4000 if (!group) return; 4001 var slug = trim(group.slug); 4002 if (!slug) return; 4003 topSlugToIds[slug] = Array.isArray(group.type_ids) 4004 ? group.type_ids.map(function (id) { return String(id); }) 4005 : []; 4006 }); 4007 4008 function idsForTopPathSlug(slug) { 4009 var s = trim(slug || ''); 4010 if (!s) return []; 4011 if (topSlugToIds[s]) return topSlugToIds[s].slice(); 4012 var sl = s.toLowerCase(); 4013 var syn = Array.isArray(cfg.topTypesSlugSynonyms) ? cfg.topTypesSlugSynonyms : []; 4014 for (var si = 0; si < syn.length; si++) { 4015 var row = syn[si]; 4016 if (!Array.isArray(row) || !row.length) continue; 4017 var inRow = false; 4018 for (var ji = 0; ji < row.length; ji++) { 4019 if (String(row[ji] || '').toLowerCase() === sl) { 4020 inRow = true; 4021 break; 4022 } 4023 } 4024 if (!inRow) continue; 4025 for (var ki = 0; ki < row.length; ki++) { 4026 var cand = trim(String(row[ki] || '')); 4027 if (cand && topSlugToIds[cand]) return topSlugToIds[cand].slice(); 4028 } 4029 } 4030 return []; 4031 } 4032 4033 /** Single estate-type slug (e.g. dvustaen-apartament) from DB `etypeSlugById`, not top-type groups. */ 4034 function idsForSingleEtypeSlug(slug) { 4035 var id = reverseLookupIdBySlug(cfg.etypeSlugById, slug, locale); 4036 return id ? [String(id)] : []; 4037 } 4038 4039 function resolvePropertyTypeIdsFromPathSlug(slug) { 4040 var fromTop = idsForTopPathSlug(slug); 4041 if (fromTop.length) return fromTop; 4042 return idsForSingleEtypeSlug(slug); 4043 } 4044 4045 var patch = Object.assign({}, base); 4046 var cityId = reverseLookupIdBySlug(cfg.citySlugById, citySlug, locale); 4047 if (cityId && !patch.city_id) patch.city_id = cityId; 4048 4049 var dealType = ''; 4050 if (dealSeg === 'prodazhbi' || dealSeg === 'for-sale' || dealSeg === 'prodazha' || dealSeg === 'verkauf') dealType = '1'; 4051 else if (dealSeg === 'naemi' || dealSeg === 'for-rent' || dealSeg === 'arenda' || dealSeg === 'miete') dealType = '2'; 4052 else if (dealSeg === 'kupuvachi') dealType = '3'; 4053 else if (dealSeg === 'naemateli') dealType = '4'; 4054 if (dealType && !patch.type) patch.type = dealType; 4055 4056 var districtId = ''; 4057 var districtGroupId = ''; 4058 var streetId = ''; 4059 var propertyTypeIds = []; 4060 if (seg2 === 'street' || seg2 === 'ulitsa' || seg2 === 'strasse' 4061 || seg2 === 'kompleks' || seg2 === 'complex' || seg2 === 'komplex') { 4062 streetId = extractStreetIdFromSlug(seg3); 4063 if (seg4) propertyTypeIds = resolvePropertyTypeIdsFromPathSlug(seg4); 4064 patch.street_namespace = String(seg2 || '').toLowerCase(); 4065 } else if (seg2) { 4066 var idsSeg2 = idsForTopPathSlug(seg2); 4067 if (idsSeg2.length) { 4068 propertyTypeIds = idsSeg2; 4069 } else { 4070 districtId = reverseLookupIdBySlug(cfg.districtSlugById, seg2, locale); 4071 if (districtId) { 4072 if (seg3) propertyTypeIds = resolvePropertyTypeIdsFromPathSlug(seg3); 4073 var dOwnerCity = trim((cfg.districtCityIdById || {})[districtId] || ''); 4074 if (dOwnerCity) { 4075 patch.city_id = dOwnerCity; 4076 } 4077 } else { 4078 districtGroupId = reverseLookupDistrictGroupIdAllLocales(cfg.districtGroupSlugById, seg2, locale); 4079 if (!districtGroupId) { 4080 districtGroupId = tryResolveDistrictGroupFromCompoundSeg(seg2, cityId, locale); 4081 } 4082 if (districtGroupId) { 4083 if (seg3) propertyTypeIds = resolvePropertyTypeIdsFromPathSlug(seg3); 4084 var gOwnerCity = trim((cfg.districtGroupCityIdById || {})[districtGroupId] || ''); 4085 if (gOwnerCity) { 4086 patch.city_id = gOwnerCity; 4087 } 4088 } else { 4089 propertyTypeIds = resolvePropertyTypeIdsFromPathSlug(seg2); 4090 if (!propertyTypeIds.length && seg3) { 4091 propertyTypeIds = resolvePropertyTypeIdsFromPathSlug(seg3); 4092 } 4093 } 4094 } 4095 } 4096 } 4097 4098 if (districtId && !patch.district_id) patch.district_id = districtId; 4099 if (districtGroupId && !patch.district_group_id) patch.district_group_id = districtGroupId; 4100 if (streetId && !patch.street_id) patch.street_id = streetId; 4101 if (propertyTypeIds.length && !patch.property_type) patch.property_type = propertyTypeIds.slice(); 4102 4103 // New-buildings SEO landing (preserve mode only): implied has_project + default Sofia so ?page=2 does not drop the flag. 4104 var citySlugNb = false; 4105 try { 4106 citySlugNb = typeof window !== 'undefined' && typeof window.__mirelaIsNbSeoCitySlug === 'function' 4107 && window.__mirelaIsNbSeoCitySlug(citySlug); 4108 } catch (_eNb0) {} 4109 if (!citySlugNb) { 4110 var csn = String(citySlug || '').toLowerCase().trim(); 4111 if (csn === 'novi-sgradi-v-stroej' || csn === 'new-buildings-under-construction' 4112 || csn === 'novye-zdaniya-v-stroitelstve' || csn === 'neubauten-im-bau') { 4113 citySlugNb = true; 4114 } 4115 } 4116 if (citySlugNb) { 4117 var nbRelPreserve = false; 4118 try { 4119 nbRelPreserve = typeof window !== 'undefined' && window.__mirelaNbLandingSeoPreserve === true; 4120 } catch (_eNbRel) {} 4121 if (nbRelPreserve) { 4122 if (!patch.city_id) patch.city_id = '3'; 4123 if (!patch.has_project) patch.has_project = '1'; 4124 } 4125 } 4126 4127 return patch; 4128 } catch (_e) { 4129 return base; 4130 } 4131 } 4132 4133 function augmentUrlSearchParams(urlParams, rel) { 4134 try { 4135 var patch = parseCanonicalListRel(rel); 4136 Object.keys(patch || {}
4136).forEach(function (key) { 4137 if (key === 'street_namespace') return; 4138 var value = patch[key]; 4139 if (value != null && typeof value === 'object' && !Array.isArray(value)) { 4140 return; 4141 } 4142 if (Array.isArray(value)) { 4143 if (urlParams.get(key + '[]') || urlParams.get(key)) return; 4144 value.forEach(function (item) { 4145 var v = trim(item); 4146 if (v) urlParams.append(key + '[]', v); 4147 }); 4148 return; 4149 } 4150 var scalar = trim(value); 4151 if (!scalar || urlParams.get(key) || urlParams.get(key + '[]')) return; 4152 urlParams.set(key, scalar); 4153 }); 4154 } catch (_e) {} 4155 return urlParams; 4156 } 4157 4158 api.parseQuery = parseQuery; 4159 api.getTopTypesGroups = getTopTypesGroups; 4160 api.reverseLookupIdBySlug = reverseLookupIdBySlug; 4161 api.extractStreetIdFromSlug = extractStreetIdFromSlug; 4162 api.parseCanonicalListRel = parseCanonicalListRel; 4163 api.augmentUrlSearchParams = augmentUrlSearchParams; 4164})(window); 4165 4166// ---- END: js/components/search-canonical-rel-parser.js ---- 4167 4168// ---- START: js/components/search-form-url-hydrator.js ---- 4169/* SSR-first search bootstrap. 4170 * 4171 * PHP renders the initial search state. This file intentionally does not hydrate 4172 * URL/session/last-list values into the form; it only exposes the legacy 4173 * completion/orchestrator signals expected by downstream modules while they are 4174 * migrated to the SSR-first contract. 4175 */ 4176(function (window, document) { 4177 'use strict'; 4178 4179 function getSearchForm() { 4180 try { 4181 var shared = window.AdvancedSearchShared || null; 4182 if (shared && typeof shared.getSearchForm === 'function') { 4183 var sharedForm = shared.getSearchForm(); 4184 if (sharedForm) return sharedForm; 4185 } 4186 } catch (_e) {} 4187 try { 4188 return document.querySelector('.js-search-form'); 4189 } catch (_e2) { 4190 return null; 4191 } 4192 } 4193 4194 function syncNiceSelectLabelsFromDom(form) { 4195 function syncOne(name) { 4196 try { 4197 var sel = form && form.querySelector ? form.querySelector('select[name="' + name + '"]') : null; 4198 if (!sel) sel = document.querySelector('select[name="' + name + '"]'); 4199 if (!sel) return; 4200 var ns = (typeof window.__mirelaNiceSelectUiRoot === 'function') 4201 ? window.__mirelaNiceSelectUiRoot(sel) 4202 : sel.nextElementSibling; 4203 if (!ns || !ns.classList || !ns.classList.contains('nice-select')) return; 4204 var options = sel.options || []; 4205 var selectedOpt = (sel.selectedIndex >= 0) ? options[sel.selectedIndex] : null; 4206 if (!selectedOpt && options.length) selectedOpt = options[0]; 4207 if (!selectedOpt) return; 4208 var label = (selectedOpt.text || selectedOpt.textContent || '').trim(); 4209 var current = ns.querySelector('.current'); 4210 if (current && label) current.textContent = label; 4211 4212 var selectedValue = String(sel.value || '').trim(); 4213 Array.from(ns.querySelectorAll('.list .option')).forEach(function (li) { 4214 li.classList.remove('selected', 'focus'); 4215 }); 4216 var targetLi = selectedValue 4217 ? ns.querySelector('.list .option[data-value="' + selectedValue.replace(/"/g, '\\"') + '"]') 4218 : ns.querySelector('.list .option[data-value=""]') || ns.querySelector('.list .option'); 4219 if (targetLi) { 4220 targetLi.classList.add('selected'); 4221 targetLi.classList.add('focus'); 4222 } 4223 } catch (_e) {} 4224 } 4225 4226 try { 4227 ['floor_from', 'floor_to', 'sea_side', 'has_upkeep', 'yearround_use'].forEach(syncOne); 4228 } catch (_e) {} 4229 } 4230 4231 function dispatchHydrationComplete() { 4232 try { 4233 var auth = window.MirelaSearch && window.MirelaSearch.searchHydrationAuthority; 4234 if (auth && typeof auth.dispatchHydrationComplete === 'function') { 4235 auth.dispatchHydrationComplete({ source: 'search-form-ssr-bootstrap' }); 4236 return; 4237 } 4238 document.dispatchEvent(new CustomEvent('mirela:search-hydration-complete', { 4239 detail: { source: 'search-form-ssr-bootstrap' } 4240 })); 4241 } catch (_e) {} 4242 } 4243 4244 function boot() { 4245 if (window.__mirelaUrlHydratorRan) return; 4246 try { window.__mirelaUrlHydratorRan = true; } catch (_eRan) {} 4247 4248 var root = window.MirelaSearch = window.MirelaSearch || {};
4249 var orch = root.searchHydrationOrchestrator = root.searchHydrationOrchestrator || {}; 4250 orch.primaryHydrationRan = true; 4251 orch.runRefreshFromLastList = orch.runRefreshFromLastList || function () {}; 4252 orch.runAfterSeoConfigScript = orch.runAfterSeoConfigScript || function () {}; 4253 orch.runPostTailInlinePhases = orch.runPostTailInlinePhases || function () { 4254 try { orch.runRefreshFromLastList(); } catch (_eRefresh) {} 4255 try { orch.runAfterSeoConfigScript(); } catch (_eSeo) {} 4256 }; 4257 4258 var form = getSearchForm(); 4259 syncNiceSelectLabelsFromDom(form); 4260 dispatchHydrationComplete(); 4261 } 4262 4263 boot(); 4264})(window, document); 4265 4266// ---- END: js/components/search-form-url-hydrator.js ---- 4267 4268// ---- START: js/search-autocomplete.js ---- 4269/** 4270 * Search Autocomplete Functionality 4271 * Provides dropdown suggestions for location search 4272 * Works for both home page and list page 4273 */ 4274 4275(function($) { 4276 'use strict'; 4277 4278 // Configuration 4279 const CONFIG = { 4280 searchDelay: 300, 4281 minSearchLength: 2, 4282 ajaxUrl: '/address', 4283 historyLimits: { search: 5, offer: 10, ac: 9, acA: 9 }, 4284 selectors: { 4285 input: '.js-search-input', 4286 suggestions: '.js-search-suggestions', 4287 locationMap: '.js-location-map', 4288 districtId: '.js-district-id', 4289 districtGroupId: '.js-district-group-id', 4290 cityId: '.js-city-id' 4291 }, 4292 /** Mark on .js-search-suggestions when list is from local history (toggle open/close on input click). */ 4293 suggestionsHistoryClass: 'suggestions-from-history', 4294 /** Clear (Ã) icon â same SVG as .pt-clear-x in js-search-form filter pills. */ 4295 clearXSvg: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' 4296 }; 4297 4298 /** Ignore keyup-driven refresh/collapse while moving through suggestions with keyboard. */ 4299 function isSuggestionNavigationKey(e) { 4300 if (!e) return false; 4301 const key = e.key || ''; 4302 if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Enter' || key === 'Escape' || key === 'Tab') { 4303 return true; 4304 } 4305 const code = e.keyCode || e.which || 0; 4306 return code === 38 || code === 40 || code === 13 || code === 27 || code === 9; 4307 } 4308 4309 /** Match app isMobile (< 1100) and mobile-location-modal.js viewport gate. */ 4310 function isMobileLocationViewport() { 4311 try { 4312 if (typeof window.__mirelaIsMobileLocationViewport === 'function') { 4313 return window.__mirelaIsMobileLocationViewport(); 4314 } 4315 } catch (_e) {} 4316 return (typeof window !== 'undefined') && window.innerWidth < 1100; 4317 } 4318 4319 // Helpers to normalize autocomplete value payloads for stable de-duplication 4320 function acNormalizeValue(val){ 4321 try{ 4322 const v = (val && typeof val === 'object') ? { ...val } : {}; 4323 const offerId = String(v.offer_id != null ? v.offer_id : '').trim(); 4324 if (v.type === 'offer' && /^\d+$/.test(offerId)) { 4325 return { type: 'offer', offer_id: offerId }; 4326 } 4327 const toStrArr = (x) => { 4328 if (x == null) return []; 4329 if (Array.isArray(x)) return x.map(s=>String(s)).filter(s=>s!== 4330 '').sort(); 4331 const s = String(x); 4332 if (s.indexOf(',') !== -1) return s.split(',').map(t=>t.trim()).filter(t=>t!=='').map(String).sort(); 4333 return s ? [s] : []; 4334 }; 4335 const out = {}; 4336 const c = toStrArr(v.cities); 4337 const d = toStrArr(v.districts); 4338 const g = toStrArr(v.district_groups); 4339 const st = (v.street_id != null && v.street_id !== '') ? String(v.street_id) : ''; 4340 if (c.length) out.cities = c; 4341 if (d.length) out.districts = d; 4342 if (g.length) out.district_groups = g; 4343 if (st) out.street_id = st; 4344 return out; 4345 }catch(_e){ return {}; } 4346 } 4347 function acValueSig(val){ try{ return JSON.stringify(acNormalizeValue(val)); }catch(_e){ return '{}'; } } 4348 4349 function makeSearchQuery(intent, options) { 4350 try { 4351 if (typeof window.makeQuery === 'function') { 4352 return window.makeQuery(intent || 'submit', options || {}); 4353 } 4354 } catch (_eGlobalQuery) {} 4355 return false; 4356 } 4357 4358 function getSearchShell() { 4359 try { 4360 var shell = window.__MIRELA_SEARCH_SHELL__; 4361 return (shell && typeof shell === 'object') ? shell : {}; 4362 } catch (_e) { 4363 return {}; 4364 } 4365 } 4366 4367 function getPageContextApi() { 4368 try { 4369 return (window.MirelaSearch && window.MirelaSearch.pageContext) ? window.MirelaSearch.pageContext : null; 4370 } catch (_e) { 4371 return null; 4372 } 4373 } 4374 4375 function isHomePageShell() { 4376 try { 4377 var shell = getSearchShell(); 4378 if (String(shell.pageKind || '').trim()) return shell.isHome === true || shell.pageKind === 'home'; 4379 } catch (_eShell) {} 4380 try { 4381 var pc = getPageContextApi(); 4382 if (pc && typeof pc.isHomePage === 'function') return !!pc.isHomePage(); 4383 } catch (_ePc) {} 4384 try { return window.isHomePage === true || !!(document.body && document.body.classList && document.body.classList.contains('page-home')); } catch (_e) { return false; } 4385 } 4386 4387 function isListPageShell() { 4388 try { 4389 var shell = getSearchShell(); 4390 if (String(shell.pageKind || '').trim()) return shell.isList === true || shell.pageKind === 'list'; 4391 } catch (_eShell) {} 4392 try { 4393 var pc = getPageContextApi(); 4394 if (pc && typeof pc.isListSearchContext === 'function') return !!pc.isListSearchContext(); 4395 if (pc && typeof pc.isListSearchContextEarly === 'function') return !!pc.isListSearchContextEarly(); 4396 } catch (_ePc) {} 4397 try { return window.isListPage === true || !!(document.querySelector('.wrap-left') && typeof window.loadOffers === 'function'); } catch (_e) { return false; } 4398 } 4399 4400 function isPreviewPageShell() { 4401 try { 4402 var shell = getSearchShell(); 4403 if (String(shell.pageKind || '').trim()) return shell.isPreview === true || shell.pageKind === 'preview'; 4404 } catch (_eShell) {} 4405 try { return window.isOfferPreviewPage === true; } catch (_e) { return false; } 4406 } 4407 4408 function isDemandsPageShell() { 4409 try { 4410 var shell = getSearchShell(); 4411 // pageKind is authoritative: preview URLs under /kupuvachi|naemateli/ also set isDemands=true in SSR shell. 4412 if (String(shell.pageKind || '').trim()) return shell.pageKind === 'demands'; 4413 } catch (_eShell) {} 4414 try { return window.isDemandsPage === true; } catch (_e) { return false; } 4415 } 4416 4417 function isExternalSearchPageShell() { 4418 try { 4419 const submitController = 4420 window.MirelaSearch && window.MirelaSearch.searchSubmitController 4421 ? window.MirelaSearch.searchSubmitController 4422 : null; 4423 return !!(submitController && typeof submitController.isExternalSearchPage === 'function' && submitController.isExternalSearchPage()); 4424 } catch (_e) { 4425 return false; 4426 } 4427 } 4428 4429 function isActualOfferPreviewPage() { 4430 try { 4431 if (!isPreviewPageShell()) return false; 4432 const route = (window.MirelaSearch && window.MirelaSearch.routeUtils) ? window.MirelaSearch.routeUtils : null; 4433 if (route && typeof route.isOfferLikePath === 'function') { 4434 return !!route.isOfferLikePath(window.location.pathname || ''); 4435 } 4436 } catch (_e) {} 4437 return /\/offer\/\d+(?:\/)?$/i.test(String(window.location.pathname || '')); 4438 } 4439 4440 function isStaticLocationPickShell() { 4441 if (isHomePageShell() || isListPageShell() || isActualOfferPreviewPage()) return false; 4442 return isExternalSearchPageShell() || isDemandsPageShell(); 4443 } 4444 4445 function shouldAutoSubmitOnSuggestionPick() { 4446 if (isActualOfferPreviewPage()) return false; 4447 if (isHomePageShell()) return false; 4448 if (isListPageShell()) return false; 4449 try { 4450 if (window.__disableAutoSubmitOnStaticSearch === true) return false; 4451 } catch (_eDis) {} 4452 try { 4453 if (window.__enableAutoSubmitOnNonAjax) return true; 4454 } catch (_eEn) {} 4455 try { 4456 const enterApi = window.MirelaSearch && window.MirelaSearch.searchInputEnterHandler; 4457 if (enterApi && typeof enterApi.isStaticSubmitPage === 'function') { 4458 return !!enterApi.isStaticSubmitPage(); 4459 } 4460 } catch (_eEnter) {} 4461 try { 4462 if (window.isOfferPreviewPage === true) return true; 4463 } catch (_ePrev) {} 4464 try { 4465 const ssc = window.MirelaSearch && window.MirelaSearch.searchSubmitController; 4466 if (ssc && typeof ssc.isStaticAutoSubmitEnabled === 'function') { 4467 return !!ssc.isStaticAutoSubmitEnabled(); 4468 } 4469 } catch (_eSsc) {} 4470 try { 4471 return !window.__MIRELA_SEARCH_SHELL__; 4472 } catch (_eShell) { 4473 return false; 4474 } 4475 } 4476 4477 function triggerAutoSubmitAfterSuggestionPick(formEl) { 4478 if (!shouldAutoSubmitOnSuggestionPick()) return; 4479 const form = formEl || (function () { 4480 try { return document.querySelector('.js-search-form'); } catch (_e) { return null; } 4481 })(); 4482 if (!form) return; 4483 setTimeout(function () { 4484 try { 4485 const ms = window.MirelaSearch; 4486 if (ms && typeof ms.triggerFormSubmitLikeSearch === 'function') { 4487 ms.triggerFormSubmitLikeSearch(form); 4488 return; 4489 } 4490 } catch (_eTrig) {} 4491 try { 4492 const enterApi = window.MirelaSearch && window.MirelaSearch.searchInputEnterHandler; 4493 if (enterApi && typeof enterApi.dispatchSyntheticSubmit === 'function') { 4494 enterApi.dispatchSyntheticSubmit(form); 4495 } 4496 } catch (_eDisp) {} 4497 }, 0); 4498 } 4499 4500 // Offer preview: selecting a location must submit like the price/area ribbons do, i.e. go through 4501 // MirelaSearch.range.queueSubmit (which runs the previewâlist redirect from the committed ids). 4502 function triggerPreviewSubmitAfterSuggestionPick(formEl) { 4503 const form = formEl || (function () { 4504 try { return document.querySelector('.js-search-form'); } catch (_e) { return null; } 4505 })(); 4506 if (!form) return; 4507 try { 4508 const ms = window.MirelaSearch; 4509 if (ms && ms.range && typeof ms.range.queueSubmit === 'function') { 4510 ms.range.queueSubmit(form); 4511 return; 4512 } 4513 } catch (_eQueue) {} 4514 try { 4515 if (typeof window.__mirelaQueueRangeSubmit === 'function') { 4516 window.__mirelaQueueRangeSubmit(form); 4517 return; 4518 } 4519 } catch (_eQueueGlobal) {} 4520 setTimeout(function () { 4521 try { 4522 const ms2 = window.MirelaSearch; 4523 if (ms2 && typeof ms2.triggerFormSubmitLikeSearch === 'function') { 4524 ms2.triggerFormSubmitLikeSearch(form); 4525 } 4526 } catch (_eTrig) {} 4527 }, 0); 4528 } 4529 4530 // expose for use in other modules 4531 try{ window.__acNormalize = acNormalizeValue; window.__acSig = acValueSig; }catch(_e){} 4532 4533 function acHistoryEffectiveTs(item, fallbackIndex) { 4534 if (item && typeof item.ts === 'number' && item.ts > 0) { return item.ts; } 4535 return typeof fallbackIndex === 'number' ? fallbackIndex : 0; 4536 } 4537 4538 function acHistoryIsInstructionLabel(label) { 4539 try { 4540 const FS = window.MirelaLocationFormSync; 4541 if (FS && typeof FS.isInstructionLocationLabel === 'function') { 4542 return FS.isInstructionLocationLabel(label); 4543 } 4544 } catch (_e) {} 4545 return false; 4546 } 4547 4548 function acHistoryNormalizeRecord(item) { 4549 const entry = Object.assign({}, item || {}); 4550 const rawValue = (entry.value && typeof entry.value === 'object') ? entry.value : {}; 4551 const rawOfferId = String(rawValue.offer_id != null ? rawValue.offer_id : '').trim(); 4552 const isOfferEntry = rawValue.type === 'offer' && /^\d+$/.test(rawOfferId); 4553 const label = String(entry.label || entry.typeahead || entry.suggestion || (isOfferEntry ? rawOfferId : '')).trim(); 4554 if (!isOfferEntry && (label.length < 3 || acHistoryIsInstructionLabel(label))) { return null; } 4555 entry.label = label; 4556 entry.typeahead = label; 4557 entry.suggestion = label; 4558 if (entry.value) { entry.value = acNormalizeValue(entry.value); } 4559 if (isOfferEntry && (!entry.value || entry.value.type !== 'offer')) { return null; } 4560 return entry; 4561 } 4562 4563 function acHistoryBuildRecord(label, value) { 4564 const text = String(label || '').trim(); 4565 return acHistoryNormalizeRecord({ 4566 label: text, 4567 typeahead: text, 4568 suggestion: text, 4569 value: value || {} 4570 }); 4571 } 4572 4573 // Lightweight history manager (localStorage-backed) 4574 const HISTORY = { 4575 KEY_SEARCH: 'search-history', 4576 KEY_OFFER: 'offer-history', 4577 KEY_AUTOCOMPLETE: 'ac-history', 4578 KEY_AUTOCOMPLETE_A: 'ac-history-a', 4579 get(key) { 4580 try { 4581 const raw = window.localStorage.getItem(key); 4582 if (!raw) return []; 4583 const arr = JSON.parse(raw); 4584 return Array.isArray(arr) ? arr : []; 4585 } catch (_e) { return []; } 4586 }, 4587 put(key, item, limit = 9) { 4588 try { 4589 const arr = this.get(key); 4590 const entry = acHistoryNormalizeRecord(item); 4591 if (!entry || !entry.value) { return; } 4592 entry.ts = Date.now(); 4593 const sig = acValueSig(entry.value ? entry.value : entry); 4594 const filtered = arr.filter(el => { 4595 try { return acValueSig(el && el.value ? el.value : el) !== sig; } catch(_e){ return true; } 4596 }); 4597 filtered.push(entry); 4598 const out = filtered 4599 .slice() 4600 .sort((a, b) => acHistoryEffectiveTs(b, 0) - acHistoryEffectiveTs(a, 0)) 4601 .slice(0, limit); 4602 window.localStorage.setItem(key, JSON.stringify(out)); 4603 } catch(_e) {} 4604 }, 4605 /** Newest-first across address + generic autocomplete keys (deduped by value signature). */ 4606 recentAutocompleteItems(limit = 9) { 4607 const combined = this.get(this.KEY_AUTOCOMPLETE_A).concat(this.get(this.KEY_AUTOCOMPLETE)); 4608 if (!combined.length) { return []; } 4609 const stamped = combined.map((it, i) => { 4610 const copy = acHistoryNormalizeRecord(it); 4611 if (!copy || !copy.value) { return null; } 4612 if (!(typeof copy.ts === 'number' && copy.ts > 0)) { copy.ts = i; } 4613 return copy; 4614 }
4614).filter(Boolean); 4615 stamped.sort((a, b) => acHistoryEffectiveTs(b, 0) - acHistoryEffectiveTs(a, 0)); 4616 const uniq = []; 4617 const seen = new Set(); 4618 for (let i = 0; i < stamped.length && uniq.length < limit; i++) { 4619 const it = stamped[i]; 4620 const sig = acValueSig(it.value ? it.value : it); 4621 if (seen.has(sig)) { continue; } 4622 seen.add(sig); 4623 uniq.push(it); 4624 } 4625 return uniq; 4626 } 4627 }; 4628 4629 try { 4630 window.__mirelaAcHistory = HISTORY; 4631 window.__mirelaAcHistoryBuildRecord = acHistoryBuildRecord; 4632 } catch (_eExpose) {} 4633 4634 class SearchAutocomplete { 4635 constructor(options = {}) { 4636 this.options = { ...CONFIG, ...options }; 4637 this.searchTimeout = null; 4638 this.$inputs = null; // all matched inputs 4639 this.$input = null; // active input 4640 this.$suggestions = null; 4641 this.$form = null; 4642 this.isInitialized = false; 4643 this.hasConfirmedSelection = false; 4644 this._autoSelecting = false; 4645 // Home-only: when user explicitly selects a suggestion, auto-trigger search ("ТÑÑÑи ÑÑк") 4646 this._homeAutoSearchPending = false; 4647 // Flag to prevent multiple search triggers from scheduled timeouts 4648 this._searchTriggerScheduled = false; 4649 // Home page has its own selection logic in `public/js/pages/home.js`. 4650 // On home we must ONLY provide suggestions rendering (typing -> /address -> dropdown), 4651 // and NOT attach selection / outside-click / submit handlers here. 4652 this._homeSuggestionsOnly = false; 4653 // Suppress autocomplete re-trigger right after a selection (ms timestamp) 4654 this.suppressUntilTs = 0; 4655 // Last real user keystroke on the location field (gates outside-click /address fallback) 4656 this._lastInputEventTs = 0; 4657 // Ignore stale /address responses when a newer fetch started or user confirmed a suggestion 4658 this._addressRequestGen = 0; 4659 this.init(); 4660 } 4661 4662 cancelPendingAddressSuggestions() { 4663 try { clearTimeout(this.searchTimeout); } catch (_e) {} 4664 this.searchTimeout = null; 4665 this._addressRequestGen = (this._addressRequestGen || 0) + 1; 4666 this.suppressUntilTs = Date.now() + 800; 4667 try { 4668 if (this.$suggestions && this.$suggestions.length) { 4669 this._collapseSuggestions(this.$suggestions); 4670 } 4671 } catch (_e) {} 4672 try { 4673 const $m = $('#mobileLocationModal .js-search-suggestions'); 4674 if ($m.length) { this._collapseSuggestions($m); } 4675 } catch (_e) {} 4676 } 4677 4678 init() { 4679 // Detect homepage early so we can run in "suggestions-only" mode. 4680 try { 4681 this._homeSuggestionsOnly = 4682 isHomePageShell(); 4683 } catch (_e) { this._homeSuggestionsOnly = false; } 4684 4685 // Prefer the mobile modal input on small screens so suggestions appear in the modal 4686 const isMobile = isMobileLocationViewport(); 4687 if (isMobile) { 4688 const $modalInput = $('#mobileLocationModal ' + this.options.selectors.input); 4689 if ($modalInput.length) { 4690 this.$inputs = $modalInput; 4691 } 4692 } 4693 // Fallback to all search inputs if mobile-specific one is not found 4694 if (!this.$inputs || !this.$inputs.length) { 4695 this.$inputs = $(this.options.selectors.input); 4696 } 4697 4698 // Initial active input context 4699 this.$input = this.$inputs.first(); 4700 this._setActiveContext(this.$input); 4701 4702 if (this.$input.length === 0 || this.$suggestions.length === 0) { 4703 // ÐÑма поле за ÑÑÑÑене на ÑÑÑаниÑаÑа 4704 return; 4705 } 4706 4707 // When the search bar is pre-filled (server-rendered, URL, saved context), treat the 4708 // value as "confirmed" so outside clicks (e.g. pagination) don't trigger the
4709 // auto-select logic which would re-open the suggestions dropdown. 4710 let initialSearchVal = ''; 4711 try { 4712 initialSearchVal = (this.$input.val() || '').toString().trim(); 4713 if (initialSearchVal.length) { this.hasConfirmedSelection = true; } 4714 } catch (_e) {} 4715 // Form already has structured location ids (SSR/URL/saved context) while the modal input may be empty. 4716 // Home/map may seed `city_id` alone for bounds â that must not count as a user-confirmed search label. 4717 try { 4718 if (this.$form && this.$form.length) { 4719 const cid = (this.$form.find('[name="city_id"]').val() || '').toString().trim(); 4720 const did = (this.$form.find('[name="district_id"]').val() || '').toString().trim(); 4721 const dgid = (this.$form.find('[name="district_group_id"]').val() || '').toString().trim(); 4722 const sid = (this.$form.find('[name="street_id"]').val() || '').toString().trim(); 4723 if (did || dgid || sid || (cid && initialSearchVal.length)) { this.hasConfirmedSelection = true; } 4724 } 4725 } catch (_e) {} 4726 4727 this.bindEvents(); 4728 this.isInitialized = true; 4729 } 4730 4731 _setActiveContext($inp) { 4732 try { 4733 if (!$inp || !$inp.length) return; 4734 this.$input = $inp; 4735 this.$suggestions = $inp.closest('.group-ip').find(this.options.selectors.suggestions).first(); 4736 if (!this.$suggestions.length && $inp.closest('#mobileLocationModal').length) { 4737 this.$suggestions = $inp.closest('#mobileLocationModal').find(this.options.selectors.suggestions).first(); 4738 } 4739 if (!this.$suggestions.length) { 4740 this.$suggestions = $(this.options.selectors.suggestions).first(); 4741 } 4742 var $closestForm = $inp.closest('form'); 4743 if ($closestForm && $closestForm.length) { 4744 this.$form = $closestForm; 4745 } else { 4746 // #mobileLocationModal: location-dropdown lives outside .js-search-form â closest('form') is empty 4747 // and setOrCreateHidden would skip; bind hiddens to the real bar form (desktop + preview). 4748 this.$form = $('.js-search-form').first(); 4749 if (!this.$form.length) { 4750 this.$form = $('.js-mobile-search-form').first(); 4751 } 4752 } 4753 } catch (_e) {} 4754 } 4755 4756 _setActiveContextFromSuggestionEl(itemEl) { 4757 try { 4758 const $item = $(itemEl); 4759 const $sugg = $item.closest(this.options.selectors.suggestions); 4760 if ($sugg && $sugg.length) { 4761 const $inp = $sugg.closest('.group-ip').find(this.options.selectors.input).first(); 4762 if ($inp && $inp.length) { 4763 this._setActiveContext($inp); 4764 return; 4765 } 4766 } 4767 } catch (_e) {} 4768 } 4769 4770 _suggestionsHistoryClass() { 4771 return String(this.options.suggestionsHistoryClass || 'suggestions-from-history'); 4772 } 4773 4774 _activeInputEl() { 4775 return this.$input && this.$input.length ? this.$input.get(0) : null; 4776 } 4777 4778 _preferredReopenTarget(inputEl) { 4779 try { 4780 const api = window.MirelaSearch && window.MirelaSearch.searchInputReopenTargetPersist; 4781 if (api && typeof api.getPersistedReopenTarget === 'function') { 4782 return api.getPersistedReopenTarget(); 4783 } 4784 if (typeof window.__mirelaGetMobileLocationModalReopenTarget === 'function') { 4785 return window.__mirelaGetMobileLocationModalReopenTarget(); 4786 } 4787 } catch (_e) {} 4788 return 'auto'; 4789 } 4790 4791 _rememberSelectionSource(source, inputEl, meta) { 4792 try { 4793 const api = window.MirelaSearch && window.MirelaSearch.searchInputReopenTargetPersist; 4794 if (api && typeof api.setFromSource === 'function') { 4795 api.setFromSource(source, meta || {}); 4796 return; 4797 } 4798 if (typeof window.__mirelaSetMobileLocationModalReopenTarget === 'function') { 4799 if (source === 'suggestion') window.__mirelaSetMobileLocationModalReopenTarget('history'); 4800 else if (source === 'location-menu' || source === 'menu') { 4801 window.__mirelaSetMobileLocationModalReopenTarget('location-menu'); 4802 } 4803 } 4804 } catch (_e) {} 4805 } 4806 4807 _resetPreferredReopenTarget(inputEl) { 4808 try { 4809 const api = window.MirelaSearch && window.MirelaSearch.searchInputReopenTargetPersist; 4810 if (api && typeof api.setFromSource === 'function') { 4811 api.setFromSource('clear'); 4812 } else if (typeof window.__mirelaSetMobileLocationModalReopenTarget === 'function') { 4813 window.__mirelaSetMobileLocationModalReopenTarget('auto'); 4814 } 4815 } catch (_e) {} 4816 } 4817 4818 _persistPreferredReopenTargetForCurrentState(inputEl) { 4819 try { 4820 const api = window.MirelaSearch && window.MirelaSearch.searchInputReopenTargetPersist; 4821 if (api && typeof api.persistBeforeNavigation === 'function') { 4822 const form = this.$form && this.$form.length ? this.$form[0] : null; 4823 const activeInput = inputEl || this._activeInputEl(); 4824 return api.persistBeforeNavigation(form, activeInput); 4825 } 4826 } catch (_eSharedPersist) {} 4827 return 'auto'; 4828 } 4829 4830 _setHistoryEligible(eligible) { 4831 const inputEl = this._activeInputEl(); 4832 if (!inputEl || !inputEl.dataset) return; 4833 4834 const next = eligible ? '1' : '0'; 4835 inputEl.dataset.historyEligible = next; 4836 4837 const dropdown = inputEl.closest ? inputEl.closest('.location-dropdown') : null; 4838 if (dropdown && dropdown.dataset) { 4839 dropdown.dataset.historyEligible = next; 4840 } 4841 } 4842 4843 _isHistoryEligibleInput() { 4844 const inputEl = this._activeInputEl(); 4845 return !!(inputEl && inputEl.dataset && inputEl.dataset.historyEligible === '1'); 4846 } 4847 4848 /** Mobile fullscreen location modal: dedicated input (not the bar clone). */ 4849 _isMobileModalLocationInput(inputEl) { 4850 try { 4851 return !!(inputEl && inputEl.closest && inputEl.closest('#mobileLocationModal')); 4852 } catch (_e) { 4853 return false; 4854 } 4855 } 4856 4857 _blurMobileModalInputAfterSuggestionPick() { 4858 try { 4859 if (!this._isMobileModalLocationInput(this._activeInputEl())) return; 4860 setTimeout(() => { 4861 try { 4862 if (typeof window.__mirelaBlurMobileModalLocationInput === 'function') { 4863 window.__mirelaBlurMobileModalLocationInput(); 4864 return; 4865 } 4866 const el = this.$input && this.$input.length ? this.$input.get(0) : null; 4867 if (el && typeof el.blur === 'function') el.blur(); 4868 } catch (_eBl) {} 4869 }, 0); 4870 } catch (_e) {} 4871 } 4872 4873 _hasVisibleInputValue(inputEl) { 4874 return !!String(inputEl && inputEl.value ? inputEl.value : '').trim(); 4875 } 4876 4877 _readStructuredLocationSelection() { 4878 const $form = this.$form && this.$form.length ? this.$form : this.$input.closest('form'); 4879 const read = (name) => { 4880 if (!$form || !$form.length) return ''; 4881 const raw = $form.find(`[name="${name}"]`).first().val(); 4882 return raw == null ? '' : String(raw).trim(); 4883 }; 4884 4885 return { 4886 cityId: read('city_id'), 4887 districtId: read('district_id'), 4888 districtGroupId: read('district_group_id'), 4889 streetId: read('street_id') 4890 }; 4891 } 4892 4893 _hasStructuredLocationSelection() { 4894 const selection = this._readStructuredLocationSelection(); 4895 return !!( 4896 selection.cityId || 4897 selection.districtId || 4898 selection.districtGroupId || 4899 selection.streetId 4900 ); 4901 } 4902 4903 /** True when only `city_id` is set and the visible search f
4903ield is empty (e.g. home map bootstrap). */ 4904 _isCityIdOnlyWithoutVisibleSearchLabel(inputEl) { 4905 if (this._hasVisibleInputValue(inputEl)) { 4906 return false; 4907 } 4908 const selection = this._readStructuredLocationSelection(); 4909 return !!( 4910 selection.cityId && 4911 !selection.districtId && 4912 !selection.districtGroupId && 4913 !selection.streetId 4914 ); 4915 } 4916 4917 maybeOpenLocationDropdownOnInputClick(inputEl) { 4918 const dropdown = inputEl && inputEl.closest ? inputEl.closest('.location-dropdown') : null; 4919 if (!dropdown || typeof dropdown.__openSelectionPanel !== 'function') { 4920 return false; 4921 } 4922 4923 // Empty field: dd-location opens only via "ÐенÑ" (.dd-toggle), never from input click. 4924 if (!this._hasVisibleInputValue(inputEl)) { 4925 return false; 4926 } 4927 4928 if (this._preferredReopenTarget(inputEl) === 'history') { 4929 return false; 4930 } 4931 4932 const menuEl = dropdown.querySelector ? dropdown.querySelector('.location-dd-menu') : null; 4933 const menuAlreadyOpen = !!( 4934 menuEl && 4935 typeof window !== 'undefined' && 4936 window.getComputedStyle && 4937 window.getComputedStyle(menuEl).display !== 'none' 4938 ); 4939 if (menuAlreadyOpen) { 4940 // location-dropdown.js is already switching away from menu mode on this click. 4941 // Re-opening the panel here would keep the menu stuck over recent-history suggestions. 4942 return false; 4943 } 4944 4945 if (this._isCityIdOnlyWithoutVisibleSearchLabel(inputEl)) { 4946 return false; 4947 } 4948 4949 const hasStructuredLocation = this._hasStructuredLocationSelection(); 4950 const hasConfirmedValue = String(inputEl.value || '').trim() !== '' && this.hasConfirmedSelection; 4951 const hasVisibleValue = this._hasVisibleInputValue(inputEl); 4952 4953 if (!hasStructuredLocation && !hasConfirmedValue && hasVisibleValue) { 4954 return false; 4955 } 4956 4957 this._setHistoryEligible(false); 4958 this._collapseSuggestions(this.$suggestions); 4959 dropdown.__openSelectionPanel(); 4960 return true; 4961 } 4962 4963 _resolveInputClickTarget(inputEl, options = {}) { 4964 const preferredTarget = this._preferredReopenTarget(inputEl); 4965 const hasVisibleValue = this._hasVisibleInputValue(inputEl); 4966 4967 // Empty field always opens recent history on input click (never dd-location). 4968 if (!hasVisibleValue) { 4969 return 'history'; 4970 } 4971 4972 if (preferredTarget === 'location-menu') return 'location-menu'; 4973 if (preferredTarget === 'history' || options.allowEmptyHistory) return 'history'; 4974 return 'none'; 4975 } 4976 4977 /** Hide and empty suggestions; clears history-panel marker used for click-toggle. */ 4978 _collapseSuggestions($el) { 4979 try { 4980 const $t = ($el && $el.length) ? $el : this.$suggestions; 4981 if ($t && $t.length) { 4982 $t.removeClass(this._suggestionsHistoryClass()).hide().empty(); 4983 } 4984 } catch (_e) {} 4985 } 4986 4987 /** History is available only after explicit X-clear on an empty field. */ 4988 toggleHistorySuggestionsOnInputClick(forceHistory) { 4989 try { 4990 const $sug = this.$suggestions; 4991 if (!$sug || !$sug.length) return; 4992 const shouldForceHistory = !!forceHistory; 4993 const inputValue = String(this.$input && this.$input.length ? this.$input.val() || '' : '').trim(); 4994 if (!shouldForceHistory && (inputValue !== '' || !this._isHistoryEligibleInput())) { 4995 this._collapseSuggestions($sug); 4996 return; 4997 } 4998 if ($sug.is(':visible') && $sug.hasClass(this._suggestionsHistoryClass())) { 4999 // Focus then click on the same field would otherwise toggle history off immediately (mobile modal). 5000 if (shouldForceHistory && this._isMobileModalLocationInput(this._activeInputEl())) { 5001 return; 5002 } 5003 this._collapseSuggestions($sug); 5004 return; 5005 } 5006 this.maybeShowRecentHistory(); 5007 } catch (_e) {} 5008 } 5009 5010 bindEvents() { 5011 // Existing selected location opens the dropdown panel; history opens only after X-clear. 5012 this.$inputs.on('focus', (ev) => { 5013 this._setActiveContext($(ev.currentTarget)); 5014 }); 5015 this.$inputs.on('click', (ev) => { 5016 this._setActiveContext($(ev.currentTarget)); 5017 try { 5018 const el = ev.currentTarget; 5019 if (this._isMobileModalLocationInput(el)) { 5020 const q = String(($(el).val() || '')).trim(); 5021 if (q === '') { 5022 // Empty modal field: focusin in mobile-location-modal.js owns first history show. 5023 return; 5024 } 5025 if (typeof window.__mobileLocationSetMode === 'function') { 5026 window.__mobileLocationSetMode('typing'); 5027 } 5028 this.toggleHistorySuggestionsOnInputClick(true); 5029 return; 5030 } 5031 } catch (_e) {} 5032 try { 5033 if (this._homeSuggestionsOnly) { 5034 const el = ev.currentTarget; 5035 if (!this._isMobileModalLocationInput(el)) { 5036 const qDesktop = String(($(el).val() || '')).trim(); 5037 if (!qDesktop) { 5038 this.toggleHistorySuggestionsOnInputClick(true); 5039 return; 5040 } 5041 } 5042 } 5043 } catch (_eHomeDesk) {} 5044 const clickTarget = this._resolveInputClickTarget(ev.currentTarget); 5045 5046 if (clickTarget === 'history') { 5047 this.toggleHistorySuggestionsOnInputClick(true); 5048 return; 5049 } 5050 5051 if (clickTarget === 'location-menu' && this.maybeOpenLocationDropdownOnInputClick(ev.currentTarget)) { 5052 try { ev.stopPropagation(); } catch (_eStop) {} 5053 return; 5054 } 5055 this.toggleHistorySuggestionsOnInputClick(false); 5056 }); 5057 // Handle input keyup 5058 this.$inputs.on('keyup input', (e) => { 5059 this._setActiveContext($(e.currentTarget)); 5060 this.handleInputKeyup(e); 5061 }); 5062 5063 // Home page: suggestions-only mode. 5064 // Do NOT bind any delegated click/blur/outside/submit handlers here to avoid double-handling 5065 // with `public/js/pages/home.js`. 5066 if (this._homeSuggestionsOnly) { 5067 // Home: иÑÑоÑÐ¸Ñ Ñамо пÑÐµÐ¼Ð°Ñ Ð²Ð°Ð½Ðµ по Ñед (X). Clear-all кÑм иÑÑоÑиÑÑа е пÑÐµÐ¼Ð°Ñ Ð½Ð°Ñ Ð¿Ð¾ пÑодÑкÑова заÑвка.
5068 // Home.js intentionally ignores clicks on `.suggestion-item-remove`, so we can safely handle them here. 5069 $(document).on('click.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item-remove', (e) => { 5070 try { 5071 e.preventDefault(); 5072 e.stopPropagation(); 5073 if (e.stopImmediatePropagation) { e.stopImmediatePropagation(); } 5074 } catch(_e) {} 5075 this._removeHistorySuggestionItem(e.currentTarget); 5076 }); 5077 5078 $(document).on('pointerdown.searchAutocomplete mousedown.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item-remove', (e) => { 5079 try { e.preventDefault(); } catch(_e) {} 5080 try { e.stopPropagation(); } catch(_e) {} 5081 if (e.stopImmediatePropagation) { try { e.stopImmediatePropagation(); } catch(_e) {} } 5082 }); 5083 5084 $(document).on('click.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item', (e) => { 5085 if ($(e.target).closest('.suggestion-item-remove').length) return; 5086 try { 5087 const $pick = $(e.currentTarget); 5088 let val = $pick.data('value'); 5089 if (typeof val === 'string') { try { val = JSON.parse(val); } catch(_e) {} } 5090 const label = ($pick.find('.suggestion-item-text').text() || $pick.text() || '').trim(); 5091 const sc = window.MirelaSearch && window.MirelaSearch.searchSubmitController; 5092 if (sc && typeof sc.tryNavigateToOfferFromHistoryPick === 'function' && 5093 sc.tryNavigateToOfferFromHistoryPick(val, label)) { 5094 try { e.preventDefault(); } catch(_e) {} 5095 try { e.stopPropagation(); } catch(_e) {} 5096 if (e.stopImmediatePropagation) { try { e.stopImmediatePropagation(); } catch(_e) {} } 5097 return; 5098 } 5099 } catch (_eHomeOffer) {} 5100 }); 5101 5102 return; 5103 } 5104 5105 // Prevent mousedown on suggestion items from blurring the input.
5106 // Without this, the blur handler hides suggestions before the click event fires, 5107 // making it impossible to select items (especially history items on empty input). 5108 this._skipNextClick = false; 5109 $(document).on('pointerdown.searchAutocomplete mousedown.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item', (e) => { 5110 // only primary button 5111 try { if (typeof e.button !== 'undefined' && e.button !== 0) { return; } } catch(_e) {} 5112 // Prevent focus loss so blur doesn't hide suggestions before click fires 5113 try { e.preventDefault(); } catch(_e) {} 5114 this._setActiveContextFromSuggestionEl(e.currentTarget); 5115 5116 // On offer preview pages, handle selection immediately on mousedown 5117 // (click may be eaten by other handlers on those pages) 5118 try { 5119 if (isActualOfferPreviewPage()) { 5120 try { e.stopPropagation(); } catch(_e) {} 5121 this._skipNextClick = true; 5122 this.suppressUntilTs = Date.now() + 800; 5123 try { clearTimeout(this.searchTimeout); } catch(_e) {} 5124 try { this.handleSuggestionClick({ currentTarget: e.currentTarget, stopPropagation: () => {} }); } catch(_e) {} 5125 } 5126 } catch (_e) {} 5127 }); 5128 5129 // Handle suggestion click 5130 $(document).on('click.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item', (e) => { 5131 // Mobile location modal has a dedicated handler in location-dropdown.js. 5132 // Skip generic autocomplete handling here to avoid state races. 5133 try { 5134 if ($(e.currentTarget).closest('#mobileLocationModal').length) { return; } 5135 } catch(_e) {} 5136 try { 5137 const $pickOffer = $(e.currentTarget); 5138 let rawOffer = $pickOffer.data('value'); 5139 if (typeof rawOffer === 'string') { try { rawOffer = JSON.parse(rawOffer); } catch(_e) {} } 5140 const pickLabel = ($pickOffer.find('.suggestion-item-text').text() || $pickOffer.text() || '').trim(); 5141 const scOfferAc = window.MirelaSearch && window.MirelaSearch.searchSubmitController; 5142 if (scOfferAc && typeof scOfferAc.tryNavigateToOfferFromHistoryPick === 'function' && 5143 scOfferAc.tryNavigateToOfferFromHistoryPick(rawOffer, pickLabel)) { 5144 try { e.preventDefault(); } catch(_e) {} 5145 try { e.stopPropagation(); } catch(_e) {} 5146 if (e.stopImmediatePropagation) { try { e.stopImmediatePropagation(); } catch(_e) {} } 5147 return; 5148 } 5149 } catch (_eOfferPick) {} 5150 // If we already handled this selection on pointerdown/mousedown, skip duplicate click 5151 if (this._skipNextClick) { 5152 this._skipNextClick = false; 5153 return; 5154 } 5155 this._setActiveContextFromSuggestionEl(e.currentTarget); 5156 // Suppress immediate re-open from synthetic input/change events 5157 this.suppressUntilTs = Date.now() + 800; 5158 try { clearTimeout(this.searchTimeout); } catch(_e) {} 5159 // Fill input + hidden ids here. On list/home/offer preview we stop propagation so 5160 // page handlers don't double-fire; submit shells auto-submit after handleSuggestionClick. 5161 try { e.preventDefault(); } catch(_e) {} 5162 try { 5163 e.stopPropagation(); 5164 if (e.stopImmediatePropagation) { e.stopImmediatePropagation(); } 5165 } catch (_e) {} 5166 try { this._collapseSuggestions(this.$suggestions); } catch(_e) {} 5167 this.handleSuggestionClick(e); 5168 }); 5169 5170 // Remove individual suggestion item via X button 5171 $(document).on('click.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item-remove', (e) => { 5172 e.preventDefault(); 5173 e.stopPropagation(); 5174 if (e.stopImmediatePropagation) { e.stopImmediatePropagation(); } 5175 this._removeHistorySuggestionItem(e.currentTarget); 5176 }); 5177 5178 // Prevent mousedown on X from blurring input 5179 $(document).on('pointerdown.searchAutocomplete mousedown.searchAutocomplete', this.options.selectors.suggestions + ' .suggestion-item-remove', (e) => { 5180 try { e.preventDefault(); } catch(_e) {} 5181 try { e.stopPropagation(); } catch(_e) {} 5182 if (e.stopImmediatePropagation) { try { e.stopImmediatePropagation(); } catch(_e) {} } 5183 }); 5184 5185 // Hide suggestions when clicking outside (or auto-select first if user typed but didn't choose) 5186 $(document).on('click', (e) => { 5187 this.handleOutsideClick(e); 5188 }); 5189 5190 // Also handle blur on the input similarly 5191 this.$inputs.on('blur', (ev) => { 5192 this._setActiveContext($(ev.currentTarget)); 5193 try {
5194 const kbAt = window.__mirelaLocationSuggestKeyboardAt || 0; 5195 if (kbAt && (Date.now() - kbAt) < 250) { 5196 return; 5197 } 5198 } catch (_eKbBlur) {} 5199 this.handleOutsideClick({ target: document.body }); 5200 }); 5201 5202 // Clear hidden location filters when input is cleared 5203 this.$inputs.on('input', (e) => { 5204 this._setActiveContext($(e.currentTarget)); 5205 this.handleInputChange(e); 5206 }); 5207 5208 // Handle form submission - bind to both the input's form and .js-search-form (may be different on mobile) 5209 const $inputForm = this.$input.closest('form'); 5210 const $jsSearchForm = $('.js-search-form'); 5211 5212 if ($inputForm.length) { 5213 $inputForm.on('submit', (e) => { 5214 this.handleFormSubmit(e); 5215 }); 5216 $inputForm.on('click', '.btn-search', (e) => { 5217 this.handleSearchButtonClick(e); 5218 }); 5219 } 5220 5221 // Also bind to .js-search-form if different from input's form (mobile modal case) 5222 if ($jsSearchForm.length && (!$inputForm.length || $jsSearchForm[0] !== $inputForm[0])) { 5223 $jsSearchForm.on('submit', (e) => { 5224 this.handleFormSubmit(e); 5225 }); 5226 $jsSearchForm.on('click', '.btn-search', (e) => { 5227 this.handleSearchButtonClick(e); 5228 }); 5229 } 5230 5231 } 5232 5233 handleInputKeyup(e) { 5234 if (isSuggestionNavigationKey(e)) { 5235 return; 5236 } 5237 // Mobile location modal mode switching: 5238 // typing in the field should force "typing" mode (hide lower menu), 5239 // clearing the field returns to default mode. 5240 try { 5241 const inMobileModal = !!(this.$input && this.$input.length && this.$input.closest('#mobileLocationModal').length); 5242 if (inMobileModal) { 5243 const q = String((this.$input.val() || '')).trim(); 5244 const modeBefore = (typeof window.__mobileLocationGetMode === 'function') 5245 ? window.__mobileLocationGetMode() 5246 : 'default'; 5247 const canSwitchFromMenu = 5248 (typeof window.__mobileLocationHasRecentTyping === 'function') 5249 ? window.__mobileLocationHasRecentTyping() 5250 : true; 5251 if (typeof window.__mobileLocationSetMode === 'function') { 5252 if (q === '') { 5253 let browseInitial = false; 5254 try { 5255 const bm = document.getElementById('mobileLocationModal'); 5256 browseInitial = !!(bm && bm.getAttribute && bm.getAttribute('data-loc-browse-initial') === '1'); 5257 } catch (_eBr) { 5258 browseInitial = false; 5259 } 5260 window.__mobileLocationSetMode(browseInitial ? 'menu' : 'default'); 5261 } else if (modeBefore !== 'menu' || canSwitchFromMenu) { 5262 window.__mobileLocationSetMode('typing'); 5263 } 5264 } 5265 const modeNow = (typeof window.__mobileLocationGetMode === 'function') 5266 ? window.__mobileLocationGetMode() 5267 : 'default'; 5268 if (modeNow === 'menu') { 5269 try { this._collapseSuggestions(this.$suggestions); } catch(_e) {} 5270 return; 5271 } 5272 } 5273 } catch(_e) {} 5274 5275 // Home suggestions-only: ignore programmatic input/change events dispatched by home.js after selection. 5276 // Otherwise the dropdown re-opens right after the map moves (user experience: "I selected Varna and it opens again"). 5277 if (this._homeSuggestionsOnly) { 5278 let trusted = false; 5279 try { trusted = !!(e && (e.isTrusted === true || (e.originalEvent && e.originalEvent.isTrusted === true))); } catch(_e) { trusted = false; } 5280 if (!trusted) { 5281 try { this.$suggestions && this._collapseSuggestions(this.$suggestions); } catch(_e) {} 5282 return; 5283 } 5284 } 5285 // If we are within suppression window after a selection, skip re-opening 5286 if (this.suppressUntilTs && Date.now() < this.suppressUntilTs) { 5287 try { clearTimeout(this.searchTimeout); } catch(_e) {} 5288 try { this._collapseSuggestions(this.$suggestions); } catch(_e) {} 5289 return; 5290 } else if (this.suppressUntilTs) { 5291 this.suppressUntilTs = 0; 5292 } 5293 const searchTerm = this.$input.val().trim(); 5294 if (searchTerm !== '') { 5295 this._setHistoryEligible(false); 5296 } 5297 this.hasConfirmedSelection = false; 5298 clearTimeout(this.searchTimeout); 5299 if (/^\d+$/.test(searchTerm)) { 5300 this._collapseSuggestions(this.$suggestions); 5301 return; 5302 } 5303 if (searchTerm.length < this.options.minSearchLength) { 5304 // For short terms â do NOT hit AJAX. History opens only via input click (toggle). 5305 this._collapseSuggestions(this.$suggestions); 5306 return; 5307 } 5308 this.searchTimeout = setTimeout(() => { 5309 this.fetchSuggestions(searchTerm); 5310 }, this.options.searchDelay); 5311 } 5312 5313 fetchSuggestions(searchTerm) { 5314 this._addressRequestGen = (this._addressRequestGen || 0) + 1; 5315 const reqGen = this._addressRequestGen; 5316 this._lastSearchTerm = String(searchTerm || '').trim(); 5317 $.ajax({
5318 url: this.options.ajaxUrl, 5319 dataType: 'json', 5320 data: { term: searchTerm }, 5321 success: (data) => { 5322 if (reqGen !== this._addressRequestGen) return; 5323 this.displaySuggestions(data); 5324 }, 5325 error: (xhr, status, error) => { 5326 if (reqGen !== this._addressRequestGen) return; 5327 this._collapseSuggestions(this.$suggestions); 5328 } 5329 }); 5330 } 5331 5332 displaySuggestions(data) { 5333 // Suppress only blocks AJAX-driven reopen right after a selection â not recent-history UI. 5334 // Otherwise the first focus/click after X-clear can hit this window and show an empty dropdown (E2E + UX). 5335 const isHistory = !!(data && data[0] && data[0].value && data[0].value.__fromHistory); 5336 if (!isHistory && this.suppressUntilTs && Date.now() < this.suppressUntilTs) { 5337 try { this._collapseSuggestions(this.$suggestions); } catch (_e) {} 5338 return; 5339 } 5340 if (data && data.length > 0) { 5341 if (!isHistory) { 5342 const term = String(this._lastSearchTerm || '').trim().toLowerCase(); 5343 const scoreSuggestion = (item) => { 5344 const label = String((item && item.label) || '').trim(); 5345 const low = label.toLowerCase(); 5346 const isRegion = /^\s*(облаÑÑ|region|обл\.|Ñегион)/i.test(label); 5347 const isCity = /^\s*(гÑад|city of|g\.|gorod|stadt)/i.test(label) || !isRegion; 5348 const normalized = low 5349 .replace(/^\s*(гÑад|city of|g\.|gorod|stadt|облаÑÑ|region|обл\.|Ñегион)\s+/i, '') 5350 .trim(); 5351 let score = 0; 5352 if (term && normalized === term) score += 100; 5353 else if (term && low.includes(term)) score += 40; 5354 if (isCity) score += 20; 5355 if (isRegion) score -= 20; 5356 return score; 5357 }; 5358 data = data.slice().sort((a, b) => scoreSuggestion(b) - scoreSuggestion(a)); 5359 } 5360 let html = ''; 5361 data.forEach((item) => { 5362 const val = JSON.stringify(item.value || {}).replace(/'/g, '''); 5363 if (isHistory) { 5364 html += `<div class="suggestion-item" data-value='${val}'>` 5365 + `<span class="suggestion-item-text">${item.label || ''}</span>` 5366 + `<button type="button" class="suggestion-item-remove pt-clear-x" title="ÐÑÐµÐ¼Ð°Ñ Ð½Ð¸" aria-label="ÐÑÐµÐ¼Ð°Ñ Ð½Ð¸ Ð¾Ñ Ð¸ÑÑоÑиÑÑа">${CONFIG.clearXSvg}</button>` 5367 + `</div>`; 5368 } else { 5369 html += `<div class="suggestion-item" data-value='${val}'>${item.label || ''}</div>`; 5370 } 5371 }); 5372 if (isHistory) { 5373 this.$suggestions.addClass(this._suggestionsHistoryClass()); 5374 } else { 5375 this.$suggestions.removeClass(this._suggestionsHistoryClass()); 5376 } 5377 this.$suggestions.html(html).show(); 5378 try { 5379 const eh = window.MirelaSearch && window.MirelaSearch.searchInputEnterHandler; 5380 const inp = this.$input && this.$input.length ? this.$input.get(0) : null; 5381 const panel = this.$suggestions && this.$suggestions.length ? this.$suggestions.get(0) : null; 5382 if (eh && typeof eh.restoreKeyboardHighlight === 'function') { 5383 eh.restoreKeyboardHighlight(inp, panel); 5384 } 5385 } catch (_eKbRestore) {} 5386 } else { 5387 this._collapseSuggestions(this.$suggestions); 5388 } 5389 5390 // Mobile location modal: respect mode switching managed by location-dropdown.js 5391 // (typing/menu/default). Keep menu hidden while typing and hidden suggestions in menu mode. 5392 try { 5393 const inMobileModal = !!(this.$input && this.$input.length && this.$input.closest('#mobileLocationModal').length); 5394 if (inMobileModal) { 5395 const mode = (typeof window.__mobileLocationGetMode === 'function') 5396 ? window.__mobileLocationGetMode() 5397 : 'default'; 5398 const hasQuery = String((this.$input.val() || '')).trim() !== ''; 5399 if (typeof window.__mobileLocationSetMode === 'function') { 5400 if (mode === 'menu' && !isHistory) { 5401 this._collapseSuggestions(this.$suggestions); 5402 } else if (hasQuery || isHistory) { 5403 // Recent-history (empty field) is a typing-shell view: hide dd-location menu. 5404 window.__mobileLocationSetMode('typing'); 5405 } else { 5406 window.__mobileLocationSetMode('default'); 5407 } 5408 } 5409 } 5410 } catch(_e) {} 5411 } 5412 5413 handleSuggestionClick(e) { 5414 const $itemEarly = $(e.currentTarget); 5415 let valueEarly = $itemEarly.data('value'); 5416 if (typeof valueEarly === 'string') { 5417 try { valueEarly = JSON.parse(valueEarly); } catch(_e) {} 5418 } 5419 const $txtEarly = $itemEarly.find('.suggestion-item-text'); 5420 const textEarly = ($txtEarly.length ? $txtEarly.text() : $itemEarly.text() || '').trim(); 5421 var scEarly = window.MirelaSearch && window.MirelaSearch.searchSubmitController; 5422 if (scEarly && typeof scEarly.tryNavigateToOfferFromHistoryPick === 'function' && 5423 scEarly.tryNavigateToOfferFromHistoryPick(valueEarly, textEarly)) { 5424 return; 5425 } 5426 try { 5427 window.__mirelaLocationCommitNoMap = true; 5428 window.__mirelaHardStopStoredHomeMapRestore = true; 5429 window.__mirelaAppliedStoredHomeMapViewOnce = true; 5430 window.__mirelaBlockStoredHomeMapRestoreUntil = Date.now() + 15000; 5431 window.__mirelaRestoringUserMapView = false; 5432 window.__mirelaExplicitLocationFit = true; 5433 window.__forceHomeAutoFitOnce = true; 5434 window.__mirelaAutocompleteSelection = true; 5435 setTimeout(function () { 5436 try { window.__mirelaExplicitLocationFit = false; } catch(_e1) {} 5437 try { window.__mirelaAutocompleteSelection = false; } catch(_e2) {} 5438 }, 2500); 5439 } catch(_e) {} 5440 const $item = $(e.currentTarget); 5441 const showMobileBaseMenu = () => { 5442 try { 5443 const inMobileModal = !!(this.$input && this.$input.length && this.$input.closest('#mobileLocationModal').length); 5444 if (!inMobileModal) return; 5445 const $menu = $('#mobileLocationModal .location-dropdown .location-dd-menu').first(); 5446 if ($menu.length) { $menu.show(); } 5447 } catch(_e) {} 5448 }; 5449 // Selecting a location from autocomplete is a hard reset of map-bounds search intent: 5450 // the subsequent /map-browse-offers request must NOT include map coords, even if the 5451 // user had earlier panned/zoomed the map or "ТÑÑÑи каÑо меÑÑиÑ" is checked. We clear 5452 // the stale URL ?map=/?map_manual= and all viewport-driven flags here â before the 5453 // user presses "ТÑÑÑи" â so every downstream code path (list.js submit handler, 5454 // triggerManualSearch, prepareListFormMapForRequest) sees a clean state. 5455 try { 5456 const _u = new URL(window.location.href); 5457 if (_u.searchParams.has('map') || _u.searchParams.has('map_manual')) { 5458 _u.searchParams.delete('map'); 5459 _u.searchParams.delete('map_manual'); 5460 history.replaceState(null, '', _u.pathname + (_u.search || '') + (_u.hash || '')); 5461 } 5462 } catch(_eStripMapUrl) {} 5463 try { window.__listBoundsMode = false; } catch(_e) {} 5464 try { window.__landedWithMap = false; } catch(_e) {} 5465 try { 5466 if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapList === 'function') { 5467 window.MirelaSearch.applyUserMovedMapList(false); 5468 } else { 5469 window.__mirelaUserMovedMap = false; 5470 } 5471 } catch(_e) {} 5472 try { window.__mapManualNext = false; } catch(_e) {} 5473 let value = $item.data('value'); 5474 // Robustly parse JSON string payloads 5475 if (typeof value === 'string') { 5476 try { value = JSON.parse(value); } catch(_e) {} 5477 } 5478 // New explicit location beats offer-preview "user cleared" session state: otherwise label 5479 // rehydration wipes the bar on next offer navigation even though the user chose again on home. 5480 try { 5481 const addrPick = !!(value && (value.cities || value.districts || value.district_groups || value.street_id)); 5482 if (addrPick) { 5483 const locState = window.MirelaSearch && window.MirelaSearch.locationState; 5484 if (locState && typeof locState.markSelected === 'function') { 5485 locState.markSelected({ label: '', value: value });
5486 } 5487 } 5488 } catch (_eClrPrev) {} 5489 5490 const $txt = $item.find('.suggestion-item-text'); 5491 const text = $txt.length ? $txt.text() : $item.text(); 5492 this.$input.val(text); 5493 try { 5494 const elPicked = this.$input.get(0); 5495 const FSscroll = window.MirelaLocationFormSync; 5496 if (FSscroll && typeof FSscroll.scrollMobileSearchInputToStart === 'function' && elPicked) { 5497 FSscroll.scrollMobileSearchInputToStart(elPicked); 5498 } 5499 if (FSscroll && typeof FSscroll.syncCompactMobileInput === 'function') { 5500 FSscroll.syncCompactMobileInput(text); 5501 } 5502 } catch (_eMobScroll) {} 5503 try { 5504 const locState2 = window.MirelaSearch && window.MirelaSearch.locationState; 5505 if (locState2 && typeof locState2.markSelected === 'function') { 5506 locState2.markSelected({ label: text, value: value || {} }); 5507 } 5508 } catch (_eLocState2) {} 5509 this._setHistoryEligible(false); 5510 const normCityId = (value && value.cities) 5511 ? String(Array.isArray(value.cities) ? (value.cities[0] || '') : value.cities).trim() 5512 : ''; 5513 this._rememberSelectionSource('suggestion', null, { cityId: normCityId, label: text }); 5514 // Mark selection BEFORE dispatching the input event so handleInputKeyup 5515 // (which runs synchronously from the dispatched event) won't schedule a 5516 // new fetchSuggestions that would re-open the dropdown after we hide it. 5517 this.hasConfirmedSelection = true; 5518 this.suppressUntilTs = Date.now() + 600; 5519 // Notify input listeners so dropdown toggle (X) appears 5520 try { 5521 const el = this.$input.get(0); 5522 if (el) { 5523 el.dispatchEvent(new Event('input', { bubbles: true })); 5524 // Do NOT dispatch 'change' here. On some pages it triggers map/search logic. 5525 // 'input' is enough to update UI (clear button). 5526 } 5527 } catch(_e) {} 5528 5529 this._blurMobileModalInputAfterSuggestionPick(); 5530 5531 // Save selection to history (Autocomplete Address history by default) 5532 try { 5533 const rec = { label: text, typeahead: text, suggestion: text, value: value || {} }; 5534 const isAddress = !!(rec.value && (rec.value.cities || rec.value.districts || rec.value.district_groups || rec.value.street_id)); 5535 const key = isAddress ? HISTORY.KEY_AUTOCOMPLETE_A : HISTORY.KEY_AUTOCOMPLETE; 5536 const limit = isAddress ? this.options.historyLimits.acA : this.options.historyLimits.ac; 5537 HISTORY.put(key, rec, limit); 5538 } catch(_e) {} 5539 5540 try { 5541 const isListPage = isListPageShell(); 5542 const isPresetPath = /-offer\d+(?:\.html)?$/i.test(String(window.location.pathname || '')); 5543 const isAddressValue = !!(value && (value.cities || value.districts || value.district_groups || value.street_id)); 5544 if (isListPage && isPresetPath && isAddressValue) { 5545 window.__listPresetBreakoutNext = true; 5546 } 5547 } catch (_ePresetBreakout) {} 5548 5549 // Offer preview pages build the target list URL from explicit id filters (city_id/district_id/...), 5550 // and intentionally drop free-text `s` (SEO). Ensure selecting an autocomplete address writes ids. 5551 // Otherwise the user sees "ÐаÑна" in the box, but the search executes with no location. 5552 // Selecting a suggestion/history item must auto-submit just like the price/area ribbons: 5553 // commit the ids, then run the previewâlist redirect via MirelaSearch.range.queueSubmit. 5554 // The mobile #mobileLocationModal flow has its own commit/OK path (location-dropdown.js), so skip it here. 5555 try { 5556 if (isActualOfferPreviewPage()) { 5557 const isAddressValue = !!(value && (value.cities || value.districts || value.district_groups || value.street_id)); 5558 if (isAddressValue) { 5559 const normCity = (value && value.cities) 5560 ? (Array.isArray(value.cities) ? (value.cities[0] || '') : value.cities) 5561 : ''; 5562 this._syncStructuredLocationHiddenFields(value, normCity || ''); 5563 } 5564 try { this._collapseSuggestions(this.$suggestions); } catch(_e) { try { this._collapseSuggestions(this.$suggestions); } catch(_e2) {} } 5565 showMobileBaseMenu(); 5566 const inMobileModalPreviewPick = !!(this.$input && this.$input.length && this.$input.closest('#mobileLocationModal').length); 5567 if (isAddressValue && !inMobileModalPreviewPick) { 5568 triggerPreviewSubmitAfterSuggestionPick(this.$form && this.$form[0] ? this.$form[0] : null); 5569 } 5570 return; 5571 } 5572 } catch (_e) {} 5573 // Static/external/demands shells: persist structured location ids when present, then auto-submit when enabled. 5574 try { 5575 if (isStaticLocationPickShell()) { 5576 const isAddressValue = !!(value && (value.cities || value.districts || value.district_groups || value.street_id)); 5577 if (isAddressValue) { 5578 const normalizedCityId = (value && value.cities) 5579 ? (Array.isArray(value.cities) ? (value.cities[0] || '') : value.cities) 5580 : ''; 5581 this._syncStructuredLocationHiddenFields(value, normalizedCityId || ''); 5582 this._syncCommittedLocationSearchLabel(text); 5583 try { this._patchUiSessionAfterLocationPick(text, value, normalizedCityId || ''); } catch (_eUiExternal) {} 5584 } 5585 try { this._collapseSuggestions(this.$suggestions); } catch (_eCollapseExternal) {} 5586 showMobileBaseMenu(); 5587 triggerAutoSubmitAfterSuggestionPick(this.$form && this.$form[0] ? this.$form[0] : null); 5588 return; 5589 } 5590 } catch (_eExternal) {} 5591 // Non-AJAX list shells (e.g. all-properties): sync hidden filters; auto-submit when shell allows it. 5592 try { 5593 if (window.__disableAjaxList) { 5594 const normCity = (value && value.cities) ? (Array.isArray(value.cities) ? (value.cities[0] || '') : value.cities) : ''; 5595 this._syncStructuredLocationHiddenFields(value, normCity || ''); 5596 try { this._collapseSuggestions(this.$suggestions); } catch(_e){} 5597 showMobileBaseMenu(); 5598 triggerAutoSubmitAfterSuggestionPick(this.$form && this.$form[0] ? this.$form[0] : null); 5599 return; 5600 } 5601 } catch(_e) {} 5602 // Do NOT write raw JSON into the hidden map input. 5603 // The map field expects a "neLng|neLat|swLng|swLat|zoom" string and 5604 // writing JSON causes parsing errors and flicker. 5605 this._collapseSuggestions(this.$suggestions); 5606 try { 5607 const inMobileModal = !!(this.$input && this.$input.length && this.$input.closest('#mobileLocationModal').length); 5608 const isListPage = (isListPageShell()); 5609 const isMobile = isMobileLocationViewport(); 5610 if (inMobileModal && isListPage && isMobile) { 5611 if (typeof window.__mirelaMobileModalAfterTopFieldSuggestion === 'function') { 5612 window.__mirelaMobileModalAfterTopFieldSuggestion(); 5613 } else { 5614 try { if (typeof window.__mobileLocationSetMode === 'function') window.__mobileLocationSetMode('typing'); } catch (_e) {} 5615 try { 5616 const r = document.getElementById('mobileLocationModal'); 5617 if (r && r.removeAttribute) r.removeAttribute('data-loc-browse-initial'); 5618 } catch (_e) {} 5619 try { 5620 const dd = document.querySelector('#mobileLocationModal .location-dropdown'); 5621 const m = dd && dd.querySelector ? dd.querySelector('.location-dd-menu') : null; 5622 if (m) m.style.display = 'none'; 5623 if (dd) dd.classList.add('open'); 5624 } catch (_e) {} 5625 } 5626 } else { 5627 showMobileBaseMenu(); 5628 } 5629 } catch (_e) { 5630 try { showMobileBaseMenu(); } catch (_e2) {} 5631 } 5632 5633 // Mobile on list page: bypass map, set city_id and trigger list load directly 5634 try { 5635 const isListPage = (isListPageShell()); 5636 const isMobile = isMobileLocationViewport(); 5637 if (isListPage && isMobile) { 5638 const $form = $('.js-search-form'); 5639 const legacyLocationCompat = window.MirelaLocationFormLegacyCompat || null; 5640 const selectionApi = window.MirelaLocationSelection || null; 5641 if (legacyLocationCompat && typeof legacyLocationCompat.applySelectionToJqueryForm === 'function') { 5642 legacyLocationCompat.applySelectionToJqueryForm($form, value, { 5643 searchLabel: text,
5644 persistCityPreset: true, 5645 ensureCityHidden: true, 5646 removeCityInputs: false, 5647 ensureLocationHidden: true, 5648 clearMap: true, 5649 forceCityOnce: true 5650 }); 5651 this._syncCommittedLocationSearchLabel(text); 5652 } else if (selectionApi && typeof selectionApi.writeLocationToForm === 'function') { 5653 const formEl = $form && $form.get ? $form.get(0) : null; 5654 const normalizedCityId = value && value.cities 5655 ? (Array.isArray(value.cities) ? value.cities[0] : value.cities) 5656 : ''; 5657 selectionApi.writeLocationToForm(formEl, { 5658 cityId: normalizedCityId || '', 5659 districtIds: value && value.districts 5660 ? (Array.isArray(value.districts) ? value.districts : [value.districts]) 5661 : [], 5662 groupIds: value && value.district_groups 5663 ? (Array.isArray(value.district_groups) ? value.district_groups : [value.district_groups]) 5664 : [], 5665 streetId: value && value.street_id ? String(value.street_id) : '', 5666 label: String(text || '').trim(), 5667 source: 'suggestion' 5668 }, { 5669 clearMap: true, 5670 syncMirrorCityInputs: true, 5671 includeStreetArrays: true, 5672 clearIndexedCityArrays: true 5673 }); 5674 this._syncCommittedLocationSearchLabel(text); 5675 } else { 5676 const ensureHidden = function (name) { 5677 let $el = $form.find('[name="' + name + '"]'); 5678 if (!$el.length) { 5679 $el = $('<input type="hidden"/>').attr('name', name).appendTo($form); 5680 } 5681 return $el; 5682 }; 5683 ensureHidden('s').val(text || ''); 5684 const normalizedCityId = value && value.cities 5685 ? Array.isArray(value.cities) 5686 ? value.cities[0] 5687 : value.cities 5688 : ''; 5689 ensureHidden('city_id').val(normalizedCityId || ''); 5690 ensureHidden('district_id').val(''); 5691 ensureHidden('district_group_id').val(''); 5692 ensureHidden('street_id').val(''); 5693 ensureHidden('map').val(''); 5694 } 5695 try { 5696 const stateApi = window.MirelaSearch && window.MirelaSearch.searchStateApi; 5697 const formEl = this.$form && this.$form[0] ? this.$form[0] : null; 5698 if (stateApi && typeof stateApi.syncFromContext === 'function' && formEl) { 5699 stateApi.syncFromContext({ form: formEl }, 'search-autocomplete.mobile-list-selection'); 5700 } 5701 } catch (_eSliceMobileList) {} 5702 // Do NOT auto-load offers here; user will press "ТÑÑÑи". 5703 return; // stop further map-bound logic 5704 } 5705 } catch(_e) {} 5706 5707 // Handle location data if available 5708 // Home page: selecting from suggestions should immediately trigger a search request 5709 // (same effect as pressing the Search button), but ONLY on explicit user selection. 5710 // IMPORTANT: Always set this flag for home page, even if it's a second selection 5711 // This ensures that when user changes location (e.g., from Banichora to Varna), 5712 // the search is triggered after the AJAX completes 5713 try { 5714 const isHome = isHomePageShell(); 5715 const isExplicitClick = !!(e && e.type === 'click'); 5716 if (isHome && isExplicitClick && !this._autoSelecting) { 5717 this._homeAutoSearchPending = true; 5718 } 5719 } catch (_e) {} 5720 5721 if (value && (value.cities || value.districts || value.street_id || value.district_groups)) { 5722 this.handleLocationSelection(value, text); 5723 } else { 5724 // If there's no structured value (rare), still allow home to search immediately. 5725 try { 5726 if (this._homeAutoSearchPending) { 5727 this._homeAutoSearchPending = false; 5728 const btn = document.querySelector('.js-map-manual'); 5729 if (btn) { try { $(btn).trigger('click'); } catch(_e) {} } 5730 } 5731 } catch(_e) {} 5732 } 5733 } 5734 5735 handleLocationSelection(value, text) { 5736 const ajaxData = { 5737 term: text, 5738 get_polygons: true 5739 }; 5740 try { 5741 const legacyLocationCompat = window.MirelaLocationFormLegacyCompat || null; 5742 if (legacyLocationCompat && typeof legacyLocationCompat.clearCommittedLocationContext === 'function' && this.$form && this.$form.length) { 5743 legacyLocationCompat.clearCommittedLocationContext(this.$form, { 5744 includeSearch: false, 5745 clearMap: true, 5746 removeMapManual: true 5747 }); 5748 } 5749 } catch (_eClearLocCtx) {} 5750 this._syncCommittedLocationSearchLabel(text); 5751 const hasPreciseLocation = !!(value && (value.districts || value.district_groups || value.street_id)); 5752 // Normalize city id (first value if array) 5753 const normalizedCityId = Array.isArray(value.cities) ? value.cities[0] : value.cities; 5754 5755 // NOTE: _homeAutoSearchPending is set in handleSuggestionClick, not here 5756 // This prevents duplicate flag setting and ensures it's only set on explicit user clicks 5757 5758 if (value.districts) { 5759 ajaxData.district_id = value.districts; 5760 ajaxData.city_id = normalizedCityId ? [normalizedCityId] : undefined; 5761 $(this.options.selectors.districtId).val(value.districts); 5762 $(this.options.selectors.districtGroupId).val(''); 5763 // Remove array-style clones from URL preservation 5764 this.$form.find('input[name="district_group_id[]"]').remove(); 5765 this.$form.find('input[name="district_id[]"]').remove(); 5766 } else if (value.district_groups) { 5767 ajaxData.district_group_id = value.district_groups; 5768 ajaxData.city_id = normalizedCityId ? [normalizedCityId] : undefined; 5769 $(this.options.selectors.districtGroupId).val(value.district_groups); 5770 $(this.options.selectors.districtId).val(''); 5771 this.$form.find('input[name="district_group_id[]"]').remove(); 5772 this.$form.find('input[name="district_id[]"]').remove(); 5773 } else if (value.street_id) { 5774 ajaxData.street_id = value.street_id; 5775 ajaxData.city_id = normalizedCityId ? [normalizedCityId] : undefined; 5776 $(this.options.selectors.districtId).val(''); 5777 $(this.options.selectors.districtGroupId).val(''); 5778 // Persist street_id so the following map request includes it 5779 this.setOrCreateHidden('street_id', value.street_id); 5780 this.$form.find('input[name="district_group_id[]"]').remove(); 5781 this.$form.find('input[name="district_id[]"]').remove(); 5782 } else if (value.cities) { 5783 ajaxData.city_id = normalizedCityId ? [normalizedCityId] : undefined; 5784 $(this.options.selectors.districtId).val(''); 5785 $(this.options.selectors.districtGroupId).val(''); 5786 this.$form.find('input[name="district_group_id[]"]').remove(); 5787 this.$form.find('input[name="district_id[]"]').remove(); 5788 } 5789 5790 // Materialize structured location ids into real hidden fields so later home/list submit 5791 // flows do not depend on pre-existing district/group/street inputs in the DOM. 5792 this._syncStructuredLocationHiddenFields(value, normalizedCityId || ''); 5793 try { 5794 if (isListPageShell()) { 5795 window.__listLocationSelectionFitNext = true; 5796 } 5797 } catch(_eFitFlag) {} 5798 if (hasPreciseLocation) { 5799 this._skipDropdownSelectionSyncOnce = true; 5800 } 5801 this.syncLocationDropdownSelectionFromForm(); 5802 try { this._patchUiSessionAfterLocationPick(text, value, normalizedCityId); } catch (_eUiS) {} 5803 try { 5804 const isHomePick = 5805 isHomePageShell(); 5806 if (isHomePick && !isListPageShell() && value && (value.districts || value.district_groups || value.street_id)) { 5807 this._clearMapHiddenForHomeStructuredPick(); 5808 } 5809 } catch (_eClearMapStr) {}
5810 this.persistCurrentHomeSelectionState(); 5811 5812 // List page city-only selection must stay canonical and city-driven. 5813 // Do not hydrate hidden viewport bounds from `/address`, otherwise later 5814 // filter/pagination/refresh flows can revive stale map-mode requests. 5815 try { 5816 const isListCityOnly = 5817 isListPageShell() && 5818 !!normalizedCityId && 5819 !value.districts && 5820 !value.district_groups && 5821 !value.street_id; 5822 if (isListCityOnly) { 5823 try { this.setOrCreateHidden('map', ''); } catch(_eMapClear) {} 5824 try { 5825 const formEl = this.$form && this.$form.get ? this.$form.get(0) : null; 5826 if (formEl) { 5827 const mapManualEl = formEl.querySelector('input[name="map_manual"]'); 5828 if (mapManualEl && mapManualEl.parentNode) { 5829 mapManualEl.parentNode.removeChild(mapManualEl); 5830 } 5831 } 5832 try { 5833 const resolveUrl = 5834 typeof window.__MIRELA_OFFERS_SEARCH_RESOLVE_URL === 'string' 5835 ? String(window.__MIRELA_OFFERS_SEARCH_RESOLVE_URL || '').trim() 5836 : ''; 5837 const msLc = window.MirelaSearch; 5838 if ( 5839 resolveUrl && 5840 msLc && 5841 typeof msLc.collectMergedOfferSearchNavigatePairs === 'function' && 5842 typeof msLc.postOffersSearchResolveCanonical === 'function' && 5843 formEl 5844 ) { 5845 const pairsLc = msLc.collectMergedOfferSearchNavigatePairs(formEl); 5846 msLc.postOffersSearchResolveCanonical(resolveUrl, pairsLc) 5847 .then(function (href) { 5848 try { 5849 const u = new URL(href, window.location.origin); 5850 history.replaceState( 5851 null, 5852 '', 5853 (u.pathname || window.location.pathname) + 5854 (u.search || '') + 5855 (u.hash || '') 5856 ); 5857 } catch (_eCanonUrl) {} 5858 }) 5859 .catch(function () { 5860 try { 5861 const u = new URL(window.location.href); 5862 u.searchParams.delete('map'); 5863 u.searchParams.delete('map_manual'); 5864 history.replaceState( 5865 null, 5866 '', 5867 u.pathname + (u.search || '') + (u.hash || '') 5868 ); 5869 } catch (_eFallbackUrl) {} 5870 }); 5871 } else { 5872 try { 5873 const u = new URL(window.location.href); 5874 u.searchParams.delete('map'); 5875 u.searchParams.delete('map_manual'); 5876 history.replaceState( 5877 null, 5878 '', 5879 u.pathname + (u.search || '') + (u.hash || '') 5880 ); 5881 } catch (_eFallbackUrl2) {} 5882 } 5883 } catch (_eUrlSync) {} 5884 } catch(_eMapManualClear) {} 5885 try { window.__listBoundsMode = false; } catch(_eBounds) {} 5886 try { window.__landedWithMap = false; } catch(_eLanded) {} 5887 try { window.__mapManualNext = false; } catch(_eMapManualNext) {} 5888 try { window.__listManualSearchNext = false; } catch(_eListManualNext) {} 5889 try { window.__manualSearchRequested = false; } catch(_eManualReq) {} 5890 try { 5891 if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapList === 'function') { 5892 window.MirelaSearch.applyUserMovedMapList(false); 5893 } else { 5894 window.__mirelaUserMovedMap = false; 5895 } 5896 } catch(_eMoved) {} 5897 try { window.__pendingLocationBounds = null; } catch(_ePendingBounds) {} 5898 try { window.__pendingLocationBoundsTs = 0; } catch(_ePendingTs) {} 5899 try { 5900 const ui = window.MirelaSearch && window.MirelaSearch.uiSession; 5901 if (ui && typeof ui.patch === 'function') { 5902 ui.patch({ map: '' }); 5903 } 5904 } catch (_eUiMapClear) {} 5905 try { this._reloadListAfterCityOnlySelection(); } catch (_eListCityReload) {} 5906 return; 5907 } 5908 } catch (_eListCityOnly) {} 5909 5910 // Fetch bounds/polygons data and store it in the hidden map input. 5911 // Client requirement: do NOT move the map / render pins / trigger searches on
5911selection. 5912 let hasLocMap = false; 5913 try { 5914 hasLocMap = 5915 !!$(this.options.selectors.locationMap).length || 5916 !!(this.$form && this.$form.find('input[name="map"]').length); 5917 } catch (_eHasMap) { 5918 hasLocMap = false; 5919 } 5920 if (!hasLocMap) { 5921 // List pages without a .js-location-map (some legacy layouts): hidden ids are already set; 5922 // still run /address + list refresh so municipality / district_group selections work. 5923 try { 5924 if ( 5925 isListPageShell() && 5926 (value.districts || value.district_groups || value.street_id) 5927 ) { 5928 const self = this; 5929 $.ajax({ 5930 url: this.options.ajaxUrl, 5931 dataType: 'json', 5932 data: ajaxData, 5933 success: function (response) { 5934 try { 5935 self._dispatchBrowseAfterLocationSelection(); 5936 } catch (_eLo) {} 5937 try { 5938 self.persistCurrentHomeSelectionState(); 5939 } catch (_ePer) {} 5940 }, 5941 error: function () { 5942 try { 5943 self._dispatchBrowseAfterLocationSelection(); 5944 } catch (_eLo2) {} 5945 } 5946 }); 5947 } 5948 } catch (_eListNoMap) {} 5949 return; 5950 } 5951 $.ajax({
5952 url: this.options.ajaxUrl, 5953 dataType: 'json', 5954 data: ajaxData, 5955 success: (response) => { 5956 try { 5957 this.handleLocationResponse(response, value); 5958 } catch (err) { 5959 // Ðко има гÑеÑка в MapLeaflet, не ÑÑпи autocomplete 5960 // console.error('Map update error:', err); 5961 } 5962 this.persistCurrentHomeSelectionState(); 5963 try { this._homeAutoSearchPending = false; } catch(_ePendingReset) {} 5964 this._dispatchBrowseAfterLocationSelection(); 5965 }, 5966 error: (xhr, status, error) => { 5967 // console.error('AJAX error (location):', error); 5968 this.persistCurrentHomeSelectionState(); 5969 try { this._homeAutoSearchPending = false; } catch(_e) {} 5970 } 5971 }); 5972 } 5973 5974 handleLocationResponse(response, value) { 5975 try { 5976 const isHomeResp = 5977 isHomePageShell(); 5978 const isListResp = 5979 isListPageShell(); 5980 const strictStructuredPick = !!(value && (value.districts || value.district_groups || value.street_id)); 5981 const skipMapHiddenForSearch = 5982 strictStructuredPick && (isHomeResp || isListResp); 5983 5984 if (skipMapHiddenForSearch) { 5985 try { this._clearMapHiddenForHomeStructuredPick(); } catch (_ePreClear) {} 5986 } 5987 5988 const writeMapBounds = (b, preferredZoom) => { 5989 try { 5990 if (skipMapHiddenForSearch) return; 5991 if (!b) return; 5992 const zoomToUse = preferredZoom || b.zoom || 14; 5993 const mapStr = [b.ne_lng, b.ne_lat, b.sw_lng, b.sw_lat, zoomToUse].join('|'); 5994 $(this.options.selectors.locationMap).val(mapStr); 5995 try { this._patchUiSessionMap(mapStr); } catch (_eUiMap) {} 5996 // Store pending bounds so we can optionally fit on explicit "ТÑÑÑи" 5997 try { window.__pendingLocationBounds = b; window.__pendingLocationBoundsZoom = zoomToUse; window.__pendingLocationBoundsTs = Date.now(); } catch(_e) {} 5998 } catch (_e) {} 5999 }; 6000 6001 // IMPORTANT: Set autocomplete selection flag BEFORE moving map 6002 // This allows fitMapBoundsSafely to move the map even if user previously moved it 6003 try { 6004 const isHome = isHomePageShell(); 6005 if (isHome) { 6006 window.__mirelaAutocompleteSelection = true; 6007 // Clear flag after a delay (give time for fitMapBoundsSafely to complete) 6008 setTimeout(() => { 6009 try { window.__mirelaAutocompleteSelection = false; } catch(_e) {} 6010 }, 2000); 6011 } 6012 } catch(_e) {} 6013 6014 // Handle district polygons 6015 if (response.district_polygons) { 6016 // IMPORTANT: Render polygons first, then move map 6017 try { 6018 const isHome = isHomePageShell(); 6019 const applyPolys = window.MirelaSearch && window.MirelaSearch.applyDistrictPolygonsToMapLeaflet; 6020 if (isHome && window.MapLeaflet && typeof applyPolys === 'function') { 6021 applyPolys(window.MapLeaflet, response, { preserveMarkers: false }); 6022 } 6023 } catch(_e) {} 6024 6025 if (response.bounds) { 6026 writeMapBounds(response.bounds); 6027 // On home page, move map to the new location bounds 6028 try { 6029 const isHome = isHomePageShell(); 6030 if (isHome && window.MapLeaflet && typeof window.MapLeaflet.fitMapBoundsSafely === 'function') { 6031 // Suppress moveend auto-search since we'll trigger manual search below 6032 try { window.__suppressNextMapAuto = true; } catch(_e) {} 6033 setTimeout(() => { 6034 try { window.__suppressNextMapAuto = false; } catch(_e) {} 6035 }, 900); 6036 window.MapLeaflet.fitMapBoundsSafely({ bounds: response.bounds }); 6037 } 6038 } catch(_e) {} 6039 } 6040 } 6041 // Handle point data (street or city) 6042 else if (response.point) { 6043 const lat = parseFloat(response.point.lat); 6044 const lng = parseFloat(response.point.lng); 6045 const isCitySearch = !!(value && (Array.isArray(value.cities) ? value.cities.length : value.cities)); 6046 const isListPage = isListPageShell(); 6047 // Slightly more zoomed out for entire city coverage on list page 6048 const preferredZoom = (isCitySearch && isListPage) ? 11 : null; 6049
6050 // Do not render a temporary marker while loading map-browse-offers results. 6051 6052 if (response.bounds) { 6053 writeMapBounds(response.bounds, preferredZoom); 6054 // On home page, move map to the new location bounds 6055 try { 6056 const isHome = isHomePageShell(); 6057 if (isHome && window.MapLeaflet && typeof window.MapLeaflet.fitMapBoundsSafely === 'function') { 6058 // Suppress moveend auto-search since we'll trigger manual search below 6059 try { window.__suppressNextMapAuto = true; } catch(_e) {} 6060 setTimeout(() => { 6061 try { window.__suppressNextMapAuto = false; } catch(_e) {} 6062 }, 900); 6063 window.MapLeaflet.fitMapBoundsSafely({ bounds: response.bounds, point: response.point }); 6064 } 6065 } catch(_e) {} 6066 } else { 6067 // Compute reasonable fallback bounds around the point and write them to map input 6068 const delta = preferredZoom ? 0.05 : 0.05; // a bit wider for city on list page 6069 const fallbackBounds = { 6070 sw_lat: lat - delta, 6071 sw_lng: lng - delta, 6072 ne_lat: lat + delta, 6073 ne_lng: lng + delta, 6074 zoom: preferredZoom || 12 6075 }; 6076 writeMapBounds(fallbackBounds); 6077 // On home page, move map to the new location bounds 6078 try { 6079 const isHome = isHomePageShell(); 6080 if (isHome && window.MapLeaflet && typeof window.MapLeaflet.fitMapBoundsSafely === 'function') { 6081 // Suppress moveend auto-search since we'll trigger manual search below 6082 try { window.__suppressNextMapAuto = true; } catch(_e) {} 6083 setTimeout(() => { 6084 try { window.__suppressNextMapAuto = false; } catch(_e) {} 6085 }, 900); 6086 window.MapLeaflet.fitMapBoundsSafely({ bounds: fallbackBounds }); 6087 } 6088 } catch(_e) {} 6089 } 6090 } 6091 // Set city ID. Some street suggestions omit value.cities but /address returns context_city_id. 6092 const responseCityId = response && response.context_city_id != null ? String(response.context_city_id || '') : ''; 6093 const valueCityId = Array.isArray(value.cities) ? (value.cities[0] || '') : (value.cities || ''); 6094 this.setOrCreateHidden('city_id', valueCityId || responseCityId); 6095 // Do NOT trigger any change-driven auto-search here - it's handled in handleLocationSelection after AJAX completes 6096 } catch (err) { 6097 // Ðко има гÑеÑка в MapLeaflet, autocomplete пÑодÑлжава да ÑабоÑи 6098 // console.error('MapLeaflet error:', err); 6099 } 6100 } 6101 6102 // If auto-search controls are not yet present (early user interaction), 6103 // trigger a manual search as soon as the controls appear, with a short timeout. 6104 triggerManualWhenControlsReady(maxWaitMs = 1500) { 6105 try { 6106 const autoCtrl = document.querySelector('.js-map-auto input[type="checkbox"]'); 6107 if (autoCtrl) { 6108 // If auto-search exists but is disabled, force a manual search 6109 if (!autoCtrl.checked) { 6110 const btn = document.querySelector('.js-map-manual'); 6111 if (btn) { 6112 try { $(btn).trigger('click'); } catch(_e) {} 6113 return; 6114 } 6115 // Manual button not yet available â keep waiting below 6116 } else { 6117 // Auto is enabled; MapLeaflet.fitMapBoundsSafely will call loadMap on moveend 6118 return; 6119 } 6120 } 6121 } catch (_e) {} 6122 const startTs = Date.now(); 6123 const tryClick = () => { 6124 const btn = document.querySelector('.js-map-manual'); 6125 if (btn) { 6126 try { $(btn).trigger('click'); } catch(_e) {} 6127 } else if (Date.now() - startTs < maxWaitMs) { 6128 setTimeout(tryClick, 80); 6129 } 6130 }; 6131 setTimeout(tryClick, 150); 6132 } 6133 6134 setOrCreateHidden(name, value) { 6135 if (!this.$form || !name) return; 6136 const $all = this.$form.find(`[name="${name}"]`); 6137 if ($all.length) { 6138 $all.val(value); 6139 } else { 6140 const $el = $('<input type="hidden"/>').attr('name', name); 6141 this.$form.append($el); 6142 $el.val(value); 6143 } 6144 if (name === 'city_id' && value) { 6145 try { document.dispatchEvent(new CustomEvent('quickLinks:cityChanged', { bubbles: true, detail: { contextCityId: value } })); } catch (_e) {} 6146 } 6147 } 6148 6149 _syncSuggestionSelectionToFilterState(value, text) { 6150 try { 6151 if (typeof window.writeLocationFilterState !== 'function') return; 6152 const normalizedCityId = Array.isArray(value && value.cities) ? value.cities[0] : (value && value.cities); 6153 window.writeLocationFilterState({ 6154 label: String(text == null ? '' : text).trim(), 6155 cityId: normalizedCityId ? String(normalizedCityId) : '', 6156 districtIds: value && value.districts 6157 ? (Array.isArray(value.districts) ? value.districts : [value.districts]).map(String).filter(
6157Boolean) 6158 : [], 6159 groupIds: value && value.district_groups 6160 ? (Array.isArray(value.district_groups) ? value.district_groups : [value.district_groups]).map(String).filter(Boolean) 6161 : [], 6162 streetId: value && value.street_id ? String(value.street_id) : '', 6163 }, { 6164 clearMap: true 6165 }); 6166 try { 6167 document.dispatchEvent(new CustomEvent('mirela:filters-state-change', { 6168 bubbles: true, 6169 detail: { type: 'suggestion-location', source: 'search-autocomplete' } 6170 })); 6171 } catch (_eEv) {} 6172 } catch (_e) {} 6173 } 6174 6175 _syncStructuredLocationHiddenFields(value, normalizedCityId) { 6176 try { 6177 if (!this.$form) return; 6178 var selectionApi = window.MirelaLocationSelection || null; 6179 var forms = []; 6180 try { 6181 var primary = this.$form && this.$form.get ? this.$form.get(0) : null; 6182 if (primary) forms.push(primary); 6183 [ 6184 document.querySelector('.js-search-form'), 6185 document.querySelector('.js-mobile-search-form') 6186 ].forEach(function (formEl) { 6187 if (!formEl || forms.indexOf(formEl) !== -1) return; 6188 forms.push(formEl); 6189 }); 6190 } catch (_eForms) {} 6191 var selection = { 6192 cityId: normalizedCityId || '', 6193 districtIds: value && value.districts 6194 ? (Array.isArray(value.districts) ? value.districts : [value.districts]) 6195 : [], 6196 groupIds: value && value.district_groups 6197 ? (Array.isArray(value.district_groups) ? value.district_groups : [value.district_groups]) 6198 : [], 6199 streetId: value && value.street_id ? String(value.street_id) : '', 6200 label: (function () { 6201 try { 6202 var existing = (this.$form.find('input[name="s"]').val() || '').toString().trim(); 6203 return existing || (this.$input && this.$input.val ? String(this.$input.val() || '').trim() : ''); 6204 } catch (_eLabel) { 6205 return ''; 6206 } 6207 }).call(this), 6208 source: 'suggestion' 6209 }; 6210 this._syncSuggestionSelectionToFilterState(value, selection.label); 6211 if (selectionApi && typeof selectionApi.mirrorLocationToForms === 'function') { 6212 selectionApi.mirrorLocationToForms(selection, forms, { 6213 clearMap: true, 6214 syncMirrorCityInputs: true, 6215 includeStreetArrays: true, 6216 clearIndexedCityArrays: true 6217 }); 6218 } else { 6219 this.setOrCreateHidden('city_id', normalizedCityId || ''); 6220 try { 6221 this.$form.find('input[name="district_id[]"], input[name="district_group_id[]"], input[name="city_id[]"]').remove(); 6222 } catch (_eArr) {} 6223 if (value && value.districts) { 6224 this.setOrCreateHidden('district_id', Array.isArray(value.districts) ? value.districts.join(',') : String(value.districts)); 6225 this.setOrCreateHidden('district_group_id', ''); 6226 this.setOrCreateHidden('street_id', ''); 6227 } else if (value && value.district_groups) { 6228 this.setOrCreateHidden('district_group_id', Array.isArray(value.district_groups) ? value.district_groups.join(',') : String(value.district_groups)); 6229 this.setOrCreateHidden('district_id', ''); 6230 this.setOrCreateHidden('street_id', ''); 6231 } else if (value && value.street_id) { 6232 this.setOrCreateHidden('street_id', String(value.street_id)); 6233 this.setOrCreateHidden('district_id', ''); 6234 this.setOrCreateHidden('district_group_id', ''); 6235 } else { 6236 this.setOrCreateHidden('district_id', ''); 6237 this.setOrCreateHidden('district_group_id', ''); 6238 this.setOrCreateHidden('street_id', ''); 6239 } 6240 } 6241 } catch (_e) {} 6242 } 6243 6244 _dispatchBrowseAfterLocationSelection() { 6245 try { 6246 if (typeof window.dispatchBrowseByPageType === 'function') { 6247 window.dispatchBrowseByPageType({ 6248 mapOnlyOptions: { defer: false } 6249 }); 6250 } 6251 } catch (_eDispatchBrowse) {} 6252 } 6253 6254 /** List city-only (СоÑиÑ/ÐаÑна from autocomplete): canonical URL sync must still reload offers. */ 6255 _reloadListAfterCityOnlySelection() { 6256 try { 6257 if (!isListPageShell()) { return false; } 6258 try { 6259 if (typeof window.resetPaginationFilterState === 'function') { 6260 window.resetPaginationFilterState(); 6261 } else if (typeof window.writePaginationFilterState === 'function') { 6262 window.writePaginationFilterState(1); 6263 } else if (typeof window.filterState === 'object' && window.filterState) { 6264 window.filterState.page = '1'; 6265 } 6266 } catch (_ePage) {} 6267 const formEl = this.$form && this.$form.get ? this.$form.get(0) : null;
6268 const listRuntime = window.MirelaSearch && window.MirelaSearch.listRuntime 6269 ? window.MirelaSearch.listRuntime 6270 : null; 6271 if (listRuntime && typeof listRuntime.makeRequest === 'function' && formEl) { 6272 return !!listRuntime.makeRequest({ 6273 reason: 'search-autocomplete.listCityOnlyCanonical', 6274 requestMode: 'location', 6275 form: formEl, 6276 force: true, 6277 append: false, 6278 resetPaging: true, 6279 explicitLocation: true 6280 }); 6281 } 6282 if (typeof window.loadOffers === 'function') { 6283 window.loadOffers(1, false, true); 6284 return true; 6285 } 6286 } catch (_e) {} 6287 this._dispatchBrowseAfterLocationSelection(); 6288 return false; 6289 } 6290 6291 /** 6292 * Keep hidden `s` aligned with the committed location label so later AJAX/canonical reads do not 6293 * fall back to a stale city-only string after a street/district/group suggestion. 6294 */ 6295 _syncCommittedLocationSearchLabel(text) { 6296 try { 6297 const label = String(text == null ? '' : text).trim(); 6298 this.setOrCreateHidden('s', label); 6299 const FS = window.MirelaLocationFormSync; 6300 if (FS && typeof FS.syncCompactMobileInput === 'function') { 6301 FS.syncCompactMobileInput(label); 6302 } 6303 } catch (_e) {} 6304 } 6305 6306 /** 6307 * Keeps MirelaSearch.uiSession in sync so canonical list/AJAX reads session before possibly stale DOM. 6308 */ 6309 _patchUiSessionAfterLocationPick(text, value, normalizedCityId) { 6310 try { 6311 const ui = window.MirelaSearch && window.MirelaSearch.uiSession; 6312 if (!ui || typeof ui.patch !== 'function') return; 6313 const p = {}; 6314 if (text !== undefined && text !== null) { 6315 p.s = String(text).trim(); 6316 } 6317 const cid = normalizedCityId != null && normalizedCityId !== '' 6318 ? String(normalizedCityId).trim() 6319 : ''; 6320 p.city_id = cid; 6321 if (value && value.districts) { 6322 const d = value.districts; 6323 p.district_id = Array.isArray(d) ? d : [d]; 6324 p.district_group_id = []; 6325 p.street_id = ''; 6326 } else if (value && value.district_groups) { 6327 const g = value.district_groups; 6328 p.district_group_id = Array.isArray(g) ? g : [g]; 6329 p.district_id = []; 6330 p.street_id = ''; 6331 } else if (value && value.street_id) { 6332 p.street_id = String(value.street_id).trim(); 6333 p.district_id = []; 6334 p.district_group_id = []; 6335 } else if (value && value.cities) { 6336 p.district_id = []; 6337 p.district_group_id = []; 6338 p.street_id = ''; 6339 } 6340 ui.patch(p); 6341 } catch (_e) {} 6342 } 6343 6344 _patchUiSessionMap(mapStr) { 6345 try { 6346 const ui = window.MirelaSearch && window.MirelaSearch.uiSession; 6347 if (!ui || typeof ui.patch !== 'function') return; 6348 const m = String(mapStr || '').trim(); 6349 if (m) { 6350 ui.patch({ map: m }); 6351 } 6352 } catch (_e) {} 6353 } 6354 6355 /** 6356 * Home: picking district / group / street from autocomplete should not send prior viewport `map` 6357 * (e.g. after "ТÑÑÑи ÑÑк"); search uses ids + label only. Map UI can still be fitted from /address without this hidden. 6358 */ 6359 _clearMapHiddenForHomeStructuredPick() { 6360 try { 6361 const isHome = 6362 isHomePageShell(); 6363 if (!isHome) return; 6364 try { if (isListPageShell()) return; } catch (_eList) {} 6365 try { this.setOrCreateHidden('map', ''); } catch (_eH) {} 6366 try { $(this.options.selectors.locationMap).val(''); } catch (_eLoc) {} 6367 try { 6368 document.querySelectorAll( 6369 '.js-search-form input[name="map"], .js-search-form .js-location-map,' + 6370 ' .js-mobile-search-form input[name="map"], .js-mobile-search-form .js-location-map' 6371 ).forEach(function (el) { 6372 try { el.value = ''; } catch (_e2) {} 6373 }); 6374 } catch (_eAll) {} 6375 try { 6376 const ui = window.MirelaSearch && window.MirelaSearch.uiSession; 6377 if (ui && typeof ui.patch === 'function') { 6378 ui.patch({ map: '' }); 6379 } 6380 } catch (_eUi) {} 6381 try { 6382 var stateApiMap = window.MirelaSearch && window.MirelaSearch.searchStateApi; 6383 if (stateApiMap && typeof stateApiMap.patchState === 'function') { 6384 stateApiMap.patchState({ filters: { map: '', map_manual: '', zoom: '' } }, 'search-autocomplete.clearMapHiddenForHomeStructuredPick'); 6385 } else if (typeof window.filterState === 'object' && window.filterState) { 6386 window.filterState.map = ''; 6387 window.filterState.map_manual = ''; 6388 window.filterState.zoom = ''; 6389 } 6390 } catch (_eSliceMap) {} 6391 try { 6392 window.__pendingLocationBounds = null; 6393 window.__pendingLocationBoundsZoom = undefined; 6394 window.__pendingLocationBoundsTs = 0; 6395 } catch (_ePb) {} 6396 } catch (_e) {} 6397 } 6398
6399 persistCurrentHomeSelectionState() {} 6400 6401 syncLocationDropdownSelectionFromForm() { 6402 try { 6403 const skipSyncOnce = this._skipDropdownSelectionSyncOnce === true; 6404 this._skipDropdownSelectionSyncOnce = false; 6405 document.querySelectorAll('.location-dropdown').forEach((dropdown) => { 6406 try { 6407 if (typeof dropdown.__resetToRoot === 'function') dropdown.__resetToRoot(); 6408 } catch (_eReset) {} 6409 try { 6410 if ( 6411 !skipSyncOnce && 6412 this._preferredReopenTarget(dropdown.querySelector('.js-location-dd-input, .js-search-input')) !== 'history' && 6413 typeof dropdown.__syncSelectionFromCurrentState === 'function' 6414 ) { 6415 dropdown.__syncSelectionFromCurrentState(); 6416 } 6417 } catch (_eSync) {} 6418 try { 6419 const menu = dropdown.querySelector('.location-dd-menu'); 6420 const toggle = dropdown.querySelector('.dd-toggle'); 6421 if (menu) menu.style.display = 'none'; 6422 if (toggle) toggle.setAttribute('aria-expanded', 'false'); 6423 dropdown.classList.remove('open'); 6424 } catch (_eClose) {} 6425 }); 6426 } catch(_e) {} 6427 } 6428 6429 _firstUsableSuggestionItem($scope) { 6430 try { 6431 const $root = $scope && $scope.length ? $scope : this.$suggestions; 6432 return $root.find('.suggestion-item').filter(function () { 6433 try { 6434 const v = (($(this).attr('data-value') || '') + '').trim(); 6435 return !!(v && v !== 'undefined'); 6436 } catch (_e) { 6437 return false; 6438 } 6439 }).first(); 6440 } catch (_e2) { 6441 return $(); 6442 } 6443 } 6444 6445 /** 6446 * Free-text in the visible field without a resolved /address suggestion is forbidden: 6447 * clear the box and wipe orphan hidden location ids driven by autocomplete. 6448 */ 6449 _rejectUncommittedAutocompleteText() { 6450 try { 6451 this.hasConfirmedSelection = false; 6452 try { 6453 this.$input.val(''); 6454 } catch (_eSet) {} 6455 try { 6456 $(this.options.selectors.districtId).val(''); 6457 $(this.options.selectors.districtGroupId).val(''); 6458 $(this.options.selectors.locationMap).val(''); 6459 this.setOrCreateHidden('street_id', ''); 6460 this.setOrCreateHidden('city_id', ''); 6461 } catch (_eHid) {} 6462 try { 6463 const el = this.$input.get(0); 6464 if (el) { 6465 el.dispatchEvent(new Event('input', { bubbles: true })); 6466 } 6467 } catch (_eEv) {} 6468 try { 6469 this.syncLocationDropdownSelectionFromForm(); 6470 } catch (_eDd) {} 6471 } catch (_e) {} 6472 } 6473 6474 handleOutsideClick(e) { 6475 // Keep suggestion panel state aligned with the visible field (modal vs bar) before any dismiss logic. 6476 try { 6477 const modalShown = typeof document !== 'undefined' 6478 ? document.querySelector('#mobileLocationModal.show') 6479 : null; 6480 if (modalShown) { 6481 const modalInp = modalShown.querySelector(this.options.selectors.input); 6482 if (modalInp) { 6483 this._setActiveContext($(modalInp)); 6484 } 6485 } 6486 } catch (_e) {} 6487 6488 // Mobile location modal: never auto-select first suggestion on outside click. 6489 // Keep behavior deterministic; selection is confirmed only by explicit tap + OK. 6490 try { 6491 const modalOpen = !!(typeof document !== 'undefined' 6492 && document.querySelector('#mobileLocationModal.show') 6493 && this.$input 6494 && this.$input.length 6495 && this.$input.closest('#mobileLocationModal').length); 6496 if (modalOpen) { 6497 if ($(e.target).closest(this.options.selectors.input + ', ' + this.options.selectors.suggestions).length) { 6498 return; 6499 } 6500 // Same as cancelPendingAddressSuggestions: this.$suggestions can point at the wrong .group-ip; 6501 // always clear the modal container + pending /address so a late response cannot reopen the list. 6502 try { 6503 if (typeof this.cancelPendingAddressSuggestions === 'function') { 6504 this.cancelPendingAddressSuggestions(); 6505 } else { 6506 this._collapseSuggestions(this.$suggestions); 6507 } 6508 } catch (_e) {} 6509 return; 6510 } 6511 } catch(_e) {} 6512 6513 // Demands pages (kupuvachi/naemateli) have their own SEO redirect logic and custom handlers. 6514 // Auto-selecting the first suggestion on outside click can: 6515 // - trigger an unexpected redirect, and 6516 // - immediately close other dropdowns (e.g. .nice-select) because it fires a synthetic click. 6517 // So on Demands pages we ONLY hide suggestions on outside click. 6518 // When invoked from blur (e.target === document.body), defer hide so the click on a suggestion
6519 // can run first; otherwise blur would hide/empty the list before the click is handled. 6520 // Use a short delay (50ms) so the click event is always processed before we hide/empty. 6521 try { 6522 if (isDemandsPageShell()) { 6523 if ($(e.target).closest(this.options.selectors.input + ', ' + this.options.selectors.suggestions).length) { 6524 return; 6525 } 6526 const self = this; 6527 if (e.target === document.body) { 6528 setTimeout(function() { 6529 try { self._collapseSuggestions(self.$suggestions); } catch(_err) { try { self._collapseSuggestions(self.$suggestions); } catch(_e2) {} } 6530 }, 50); 6531 } else { 6532 try { this._collapseSuggestions(this.$suggestions); } catch(_e) { try { this._collapseSuggestions(this.$suggestions); } catch(_e2) {} } 6533 } 6534 return; 6535 } 6536 } catch (_e) {} 6537 6538 // Offer preview: do not commit on arbitrary page clicks (can fire alongside map/UI). 6539 // Blur is routed through handleOutsideClick({ target: document.body }) â allow commit there. 6540 try { 6541 if (isActualOfferPreviewPage()) { 6542 if ($(e.target).closest(this.options.selectors.input + ', ' + this.options.selectors.suggestions).length) { 6543 return; 6544 } 6545 if (e.target !== document.body) { 6546 try { this._collapseSuggestions(this.$suggestions); } catch(_ePv) { try { this._collapseSuggestions(this.$suggestions); } catch(_ePv2) {} } 6547 return; 6548 } 6549 } 6550 } catch (_e) {} 6551 6552 if ($(e.target).closest(this.options.selectors.input + ', ' + this.options.selectors.suggestions).length) { 6553 return; 6554 } 6555 const term = (this.$input.val() || '').trim(); 6556 // Label from DD menu / list SSR (hidden city_id et al.) is not "orphan typing". 6557 // Blur-used-for-outside-click must not fake a suggestion pick â that flips reopen target to `history`. 6558 const inputEl = this.$input && this.$input.length ? this.$input.get(0) : null; 6559 const committedStructuredLocation = !!( 6560 inputEl && 6561 this._hasStructuredLocationSelection() && 6562 !this._isCityIdOnlyWithoutVisibleSearchLabel(inputEl) 6563 ); 6564 // Requirement: if user typed something and clicks outside without choosing, 6565 // auto-select the first suggestion, BUT do NOT auto-search. 6566 if (term.length >= this.options.minSearchLength && !this.hasConfirmedSelection && !committedStructuredLocation) { 6567 if (this._autoSelecting) { return; } 6568 this._autoSelecting = true; 6569 const $first = this._firstUsableSuggestionItem(this.$suggestions); 6570 if ($first && $first.length) { 6571 try { 6572 // Do NOT trigger a real click (it can bubble to page handlers). 6573 this.handleSuggestionClick({ currentTarget: $first.get(0) }); 6574 } catch(_e) { try { this.handleSuggestionClick({ currentTarget: $first.get(0) }); } catch(_e2) {} } 6575 this._autoSelecting = false; 6576 try { this._collapseSuggestions(this.$suggestions); } catch(_e) { try { this._collapseSuggestions(this.$suggestions); } catch(_e2) {} } 6577 return; 6578 } 6579 // No visible suggestions in DOM â fetch and pick first (even if typing was earlier; 6580 // otherwise free-text orphans would linger without a backing /address payload). 6581 $.ajax({
6582 url: this.options.ajaxUrl, 6583 dataType: 'json', 6584 data: { term }, 6585 success: (data) => { 6586 try { 6587 if (Array.isArray(data) && data.length) { 6588 const usableRows = []; 6589 try { 6590 for (let i = 0; i < data.length; i++) { 6591 const row = data[i]; 6592 if (row && row.value) { usableRows.push(row); } 6593 } 6594 } catch (_eFil) {} 6595 if (usableRows.length) { 6596 try { this.displaySuggestions(usableRows); } catch (_eDisp) {} 6597 } 6598 const $fi = this._firstUsableSuggestionItem(this.$suggestions); 6599 if ($fi && $fi.length) { 6600 try { this.handleSuggestionClick({ currentTarget: $fi.get(0) }); } catch(_e) { try { this.handleSuggestionClick({ currentTarget: $fi.get(0) }); } catch(_e2) {} } 6601 this._autoSelecting = false; 6602 try { this._collapseSuggestions(this.$suggestions); } catch(_e) { try { this._collapseSuggestions(this.$suggestions); } catch(_e2) {} } 6603 return; 6604 } 6605 } 6606 } catch(_e) {} 6607 this._collapseSuggestions(this.$suggestions); 6608 this._autoSelecting = false; 6609 this._rejectUncommittedAutocompleteText(); 6610 }, 6611 error: () => { 6612 try { this._collapseSuggestions(this.$suggestions); } catch(_eCe) {} 6613 this._autoSelecting = false; 6614 this._rejectUncommittedAutocompleteText(); 6615 } 6616 }); 6617 return; 6618 } 6619 this._collapseSuggestions(this.$suggestions); 6620 } 6621 6622 handleInputChange(e) { 6623 const val = this.$input.val().trim(); 6624 const isTrustedInput = !!(e && (e.isTrusted === true || (e.originalEvent && e.originalEvent.isTrusted === true))); 6625 try { 6626 if (Date.now() < (this.suppressUntilTs || 0)) { 6627 if (val.length >= this.options.minSearchLength) { 6628 this.hasConfirmedSelection = true; 6629 } 6630 return; 6631 } 6632 this._lastInputEventTs = Date.now(); 6633 } catch (_e) {} 6634 this.hasConfirmedSelection = false; 6635 if (isTrustedInput) { 6636 this._resetPreferredReopenTarget(); 6637 } 6638 if (val.length > 0) { 6639 this._setHistoryEligible(false); 6640 } 6641 try { window.__listLocationSelectionFitNext = false; } catch(_eFlag) {} 6642 // On any typing, clear previous street filter so it doesn't leak into next search 6643 this.setOrCreateHidden('street_id', ''); 6644 if (val.length < this.options.minSearchLength) { 6645 $(this.options.selectors.districtId).val(''); 6646 $(this.options.selectors.districtGroupId).val(''); 6647 $(this.options.selectors.locationMap).val(''); 6648 // Also clear city when the input is cleared 6649 this.setOrCreateHidden('city_id', ''); 6650 // History list is click-toggled only; do not auto-open on clear/short input. 6651 this._collapseSuggestions(this.$suggestions); 6652 } 6653 } 6654 6655 handleFormSubmit(e) { 6656 const searchValue = String(this.$input && this.$input.val() ? this.$input.val() : '').trim(); 6657 this._persistPreferredReopenTargetForCurrentState(); 6658 if (/^\d+$/.test(searchValue) && searchValue.length > 0) { 6659 e.preventDefault(); 6660 this._storeOfferSearchIfListPage(searchValue); 6661 const isListCtx = isListPageShell(); 6662 if (isListCtx) { 6663 this._prepareListFormForOfferIdSearch(searchValue); 6664 this._resolveOfferUrlById( 6665 searchValue, 6666 (href) => { 6667 try { window.location.href = href; } catch (_eNav) {} 6668 }, 6669 () => { 6670 this._redirectNumericOfferIdSearch(searchValue); 6671 } 6672 ); 6673 } else { 6674 this._redirectNumericOfferIdSearch(searchValue); 6675 } 6676 return; 6677 } 6678 6679 try { 6680 if (isDemandsPageShell()) { 6681 // Demands pages own their redirect flow (including agent demands exit-to-normal-list).
6682 // Do not treat them as generic "external pages". 6683 return; 6684 } 6685 } catch (_eDemands) {} 6686 // External pages (e.g. /cenite): do not hijack submit. 6687 // Build canonical locale-aware list URLs directly here. 6688 try { 6689 const isHome = isHomePageShell(); 6690 const isList = isListPageShell(); 6691 if (!isHome && !isList) { 6692 const form = this.$form && this.$form.length ? this.$form[0] : (this.$input && this.$input.length ? this.$input.closest('form')[0] : null); 6693 if (!form) return; 6694 e.preventDefault(); 6695 this._redirectExternalSearchToCanonicalList(form); 6696 return; 6697 } 6698 } catch (_eCtx) {} 6699 6700 // Persist custom search (no AJAX autocomplete) in history 6701 try { 6702 // Build value payload from hidden inputs if available 6703 const $form = this.$form || this.$input.closest('form'); 6704 const get = (n) => ($form && $form.find(`[name="${n}"]`).val()) || ''; 6705 const cityId = get('city_id'); 6706 const districtsCsv = get('district_id'); 6707 const groupsCsv = get('district_group_id'); 6708 const streetId = get('street_id'); 6709 const value = {}; 6710 if (cityId) { value.cities = Array.isArray(cityId) ? cityId : [cityId]; } 6711 if (districtsCsv) { value.districts = String(districtsCsv).split(',').filter(Boolean).map(s=>s.trim()); } 6712 if (groupsCsv) { value.district_groups = String(groupsCsv).split(',').filter(Boolean).map(s=>s.trim()); } 6713 if (streetId) { value.street_id = streetId; } 6714 const rec = { label: searchValue || (value.cities ? 'â' : ''), typeahead: searchValue, suggestion: searchValue, value }; 6715 const isAddress = !!(value.cities || value.districts || value.district_groups || value.street_id); 6716 const key = isAddress ? HISTORY.KEY_AUTOCOMPLETE_A : HISTORY.KEY_AUTOCOMPLETE; 6717 const limit = isAddress ? this.options.historyLimits.acA : this.options.historyLimits.ac; 6718 // Store only if 3+ chars OR address selection present 6719 if ((searchValue && searchValue.length > 3) || isAddress) { 6720 HISTORY.put(key, rec, limit); 6721 } 6722 } catch(_e) {} 6723 try { if (window.__disableAjaxList) { return; } } catch(_e) {} 6724 // Home page: move map to pending bounds (if any), then search 6725 try { 6726 if (isHomePageShell()) { 6727 e.preventDefault(); 6728 this._applyPendingBoundsAndSearch(); 6729 return; 6730 } 6731 } catch(_e) {} 6732 e.preventDefault(); 6733 this.triggerManualSearch(); 6734 } 6735 6736 handleSearchButtonClick(e) { 6737 try { 6738 const target = e && e.target && e.target.closest ? e.target.closest('.js-home-list-cta') : null; 6739 if (target) { 6740 return; 6741 } 6742 } catch (_eHomeListCta) {} 6743 6744 const searchValueBtn = String(this.$input && this.$input.val() ? this.$input.val() : '').trim(); 6745 this._persistPreferredReopenTargetForCurrentState(); 6746 if (/^\d+$/.test(searchValueBtn) && searchValueBtn.length > 0) { 6747 e.preventDefault(); 6748 this._storeOfferSearchIfListPage(searchValueBtn); 6749 const isListCtxBtn = isListPageShell(); 6750 if (isListCtxBtn) { 6751 this._prepareListFormForOfferIdSearch(searchValueBtn); 6752 this._resolveOfferUrlById( 6753 searchValueBtn, 6754 (href) => { 6755 try { window.location.href = href; } catch (_eNav) {} 6756 }, 6757 () => { 6758 this._redirectNumericOfferIdSearch(searchValueBtn); 6759 } 6760 ); 6761 } else { 6762 this._redirectNumericOfferIdSearch(searchValueBtn); 6763 } 6764 return; 6765 } 6766 6767 try { 6768 if (isDemandsPageShell()) { 6769 // Let demands-specific handlers build the target URL. 6770 return; 6771 } 6772 } catch (_eDemands) {} 6773 // External pages (e.g. /cenite): do not hijack click submit flow. 6774 // Build canonical locale-aware list URLs directly here. 6775 try { 6776 const isHome = isHomePageShell(); 6777 const isList = isListPageShell(); 6778 if (!isHome && !isList) { 6779 const form = this.$form && this.$form.length ? this.$form[0] : (this.$input && this.$input.length ? this.$input.closest('form')[0] : null); 6780 if (!form) return; 6781 e.preventDefault(); 6782 this._redirectExternalSearchToCanonicalList(form); 6783 return; 6784 } 6785 } catch (_eCtx) {} 6786 6787 // Home page: move map to pending bounds (if any), then search 6788 try { 6789 if (isHomePageShell()) { 6790 e.preventDefault(); 6791 this._applyPendingBoundsAndSearch(); 6792 return; 6793 } 6794 } catch(_e) {} 6795 try { if (window.__disableAjaxList) { return; } } catch(_e) {} 6796 e.preventDefault(); 6797 this.triggerManualSearch(); 6798 } 6799 6800 _redirectExternalSearchToCanonicalList(form) { 6801 try { 6802 try { 6803 var flushNav = 6804 window.MirelaSearch && 6805 typeof window.MirelaSearch.flushOfferSearchNavigateFormDom === 'function' 6806 ? window.MirelaSearch.flushOfferSearchNavigateFormDom 6807 : null; 6808 if (flushNav) flushNav(form); 6809 } catch (_eUnifiedFlushAc) {} 6810 try { 6811 if (!(window.MirelaSearch && typeof window.MirelaSearch.flushOfferSearchNavigateFormDom === 'function')) { 6812 try { 6813 var SharedAdvAc = window.AdvancedSearchShared; 6814 if (SharedAdvAc && typeof SharedAdvAc.syncAdvancedPanelStateToForm === 'function') { 6815 SharedAdvAc.syncAdvancedPanelStateToForm({ 6816 form: form, 6817 panel: document.querySelector('.wd-search-form') 6818 }); 6819 } 6820 } catch (_eAcFlush) {} 6821 try { 6822 var trfAc = window.MirelaSearch && window.MirelaSearch.topRangeFilters; 6823 if (trfAc && typeof trfAc.flushRibbonRangesIntoForm === 'function') { 6824 trfAc.flushRibbonRangesIntoForm(form); 6825 } 6826 } catch (_eRibbonFlushAc) {} 6827 } 6828 } catch (_eLegacyFlushFallback) {} 6829 const msExt = window.MirelaSearch; 6830 if (msExt && typeof msExt.tryOffersSearchServerListNavigationFromForm === 'function') { 6831 const navExt = msExt.tryOffersSearchServerListNavigationFromForm(form); 6832 if (navExt && navExt.handled) { 6833 if (!navExt.sync && navExt.promise && typeof navExt.promise.catch === 'function') { 6834 navExt.promise.catch(function () { 6835 try { 6836 form.submit(); 6837 } catch (__e) {} 6838 }); 6839 } 6840 return; 6841 } 6842 } 6843 } catch(_e) { 6844 try { form.submit(); } catch(__e) {} 6845 } 6846 try { form.submit(); } catch(__es) {} 6847 } 6848 6849 // Helper: Store offer ID so preview page can restore it in the search input 6850 // (used when navigating to offer by number from home, list, or any page) 6851 _storeOfferSearchIfListPage(offerId) { 6852 try { 6853 if (typeof window !== 'undefined' && offerId) { 6854 const offerHint = window.MirelaSearch && window.MirelaSearch.offerIdSearchHint; 6855 if (offerHint && typeof offerHint.set === 'function') offerHint.set(String(offerId)); 6856 } 6857 } catch(_e) {} 6858 } 6859 6860 _redirectNumericOfferIdSearch(offerId) { 6861 try { 6862 const id = String(offerId || '').trim(); 6863 if (!id) return; 6864 const submitController = 6865 window.MirelaSearch && window.MirelaSearch.searchSubmitController 6866 ? window.MirelaSearch.searchSubmitController 6867 : null; 6868 if (submitController && typeof submitController.resolveOfferRedirectUrl === 'function') { 6869 const href = String( 6870 submitController.resolveOfferRedirectUrl(id, window.location.pathname || '') || '' 6871 ).trim(); 6872 if (href) { 6873 window.location.href = href; 6874 return; 6875 } 6876 } 6877 } catch (_e) {} 6878 try { 6879 window.location.href = '/offer/' + encodeURIComponent(String(offerId || '').trim()); 6880 } catch (_e2) {} 6881 } 6882 6883 _prepareListFormForOfferIdSearch(offerId) { 6884 try { 6885 const form = document.querySelector('.js-search-form'); 6886 if (!form) return; 6887 const clearScalar = (name) => { 6888 try { 6889 const el = form.querySelector(`input[name="${name}"]`); 6890 if (el) el.value = ''; 6891 } catch(_e) {} 6892 }; 6893 clearScalar('city_id'); 6894 clearScalar('district_id'); 6895 clearScalar('district_group_id'); 6896 clearScalar('street_id'); 6897 clearScalar('map'); 6898 6899 try { 6900 const mapMirror = form.querySelector('.js-location-map'); 6901 if (mapMirror) mapMirror.value = ''; 6902 } catch(_eMap) {} 6903 6904 try { 6905 form.querySelectorAll('input[name="district_id[]"], input[name="district_group_id[]"], input[name="city_id[]"]').forEach((el) => { 6906 try { el.remove(); } catch(_eRm) {} 6907 }); 6908 } catch(_eArr) {} 6909 6910 try { 6911 let sEl = form.querySelector('input[name="s"]'); 6912 if (!sEl) { 6913 sEl = document.createElement('input'); 6914 sEl.type = 'hidden'; 6915 sEl.name = 's'; 6916 form.appendChild(sEl); 6917 }
6918 sEl.value = String(offerId || '').trim(); 6919 } catch(_eS) {} 6920 } catch(_e) {} 6921 } 6922 6923 _collectSearchFormPayload() { 6924 const payload = {}; 6925 try { 6926 const form = document.querySelector('.js-search-form'); 6927 if (!form) return payload; 6928 const fd = new FormData(form); 6929 for (const [k, v] of fd.entries()) { 6930 const key = String(k || '').trim(); 6931 if (!key || key === '_token') continue; 6932 const val = (v == null) ? '' : String(v).trim(); 6933 if (!val) continue; 6934 if (Object.prototype.hasOwnProperty.call(payload, key)) { 6935 if (Array.isArray(payload[key])) payload[key].push(val); 6936 else payload[key] = [payload[key], val]; 6937 } else { 6938 payload[key] = val; 6939 } 6940 } 6941 } catch(_e) {} 6942 return payload; 6943 } 6944 6945 _extractOfferHrefFromListHtml(listHtml, offerId) { 6946 try { 6947 const id = String(offerId || '').trim(); 6948 if (!id || !listHtml) return ''; 6949 const holder = document.createElement('div'); 6950 holder.innerHTML = String(listHtml || ''); 6951 const byCard = holder.querySelector(`[data-offer-id="${id.replace(/"/g, '')}"] a[href]`); 6952 if (byCard && byCard.getAttribute) { 6953 const href = String(byCard.getAttribute('href') || '').trim(); 6954 if (href) return href; 6955 } 6956 const bySlug = holder.querySelector(`a[href*="-${id}"], a[href*="/offer/${id}"]`); 6957 if (bySlug && bySlug.getAttribute) { 6958 const href2 = String(bySlug.getAttribute('href') || '').trim(); 6959 if (href2) return href2; 6960 } 6961 } catch(_e) {} 6962 return ''; 6963 } 6964 6965 _resolveOfferUrlById(offerId, onResolved, onFallback) { 6966 try { 6967 if (window.__offerIdResolveInProgress) { 6968 if (typeof onFallback === 'function') onFallback(); 6969 return; 6970 } 6971 window.__offerIdResolveInProgress = true; 6972 const payload = this._collectSearchFormPayload(); 6973 payload.s = String(offerId || '').trim(); 6974 try { delete payload.map; } catch(_eMap) {} 6975 try { delete payload.map_manual; } catch(_eMap2) {} 6976 const finish = () => { try { window.__offerIdResolveInProgress = false; } catch(_eFlag) {} }; 6977 window.sendMapBrowseOffers({ 6978 payload, 6979 success: (resp) => { 6980 let href = ''; 6981 try { href = this._extractOfferHrefFromListHtml(resp && resp.list_html, offerId); } catch(_eHref) {} 6982 finish(); 6983 if (href && typeof onResolved === 'function') { onResolved(href); return; } 6984 if (typeof onFallback === 'function') onFallback(); 6985 } 6986 }).fail(() => { 6987 finish(); 6988 if (typeof onFallback === 'function') onFallback(); 6989 }); 6990 } catch(_e) { 6991 try { window.__offerIdResolveInProgress = false; } catch(_eFlag2) {} 6992 if (typeof onFallback === 'function') onFallback(); 6993 } 6994 } 6995 6996 // Apply pending location bounds (from autocomplete selection) to the map and trigger search. 6997 // Used by both home and list pages when "ТÑÑÑи" is pressed. 6998 _applyPendingBoundsAndSearch() { 6999 // Home: polygon location (district / group / street) should open the SEO list path 7000 // (e.g. /prodazhbi/grad-sofia/mladost) via submit-controller rollup â not only map-browse. 7001 try { 7002 if (isHomePageShell()) { 7003 const form = document.querySelector('.js-search-form'); 7004 if (form) { 7005 const getV = (sel) => { 7006 try { 7007 return String((form.querySelector(sel) && form.querySelector(sel).value) || '').trim(); 7008 } catch (_e) { 7009 return ''; 7010 } 7011 }; 7012 const hasDistrict = 7013 getV('input[name="district_id"]') !== '' || 7014 form.querySelectorAll('input[name="district_id[]"]').length > 0;
7015 const hasGroup = 7016 getV('input[name="district_group_id"]') !== '' || 7017 form.querySelectorAll('input[name="district_group_id[]"]').length > 0; 7018 const hasStreet = getV('input[name="street_id"]') !== ''; 7019 if (hasDistrict || hasGroup || hasStreet) { 7020 const msH = window.MirelaSearch; 7021 const hForm = document.querySelector('.js-search-form'); 7022 if ( 7023 msH && 7024 typeof msH.tryOffersSearchServerListNavigationFromForm === 'function' && 7025 hForm 7026 ) { 7027 const navH = msH.tryOffersSearchServerListNavigationFromForm(hForm); 7028 if (navH && navH.handled) { 7029 if (!navH.sync && navH.promise && typeof navH.promise.catch === 'function') { 7030 navH.promise.catch(function () {}); 7031 } 7032 return; 7033 } 7034 } 7035 } 7036 } 7037 } 7038 } catch (_eHomeList) {} 7039 try { 7040 const pb = window.__pendingLocationBounds; 7041 const pbTs = window.__pendingLocationBoundsTs || 0; 7042 if (pb && (Date.now() - pbTs < 300000)) { 7043 window.__pendingLocationBounds = null; 7044 window.__pendingLocationBoundsTs = 0; 7045 const zoom = window.__pendingLocationBoundsZoom || pb.zoom || 14; 7046 // Write bounds to hidden map input 7047 try { 7048 const mapEl = document.querySelector('.js-search-form input[name="map"]'); 7049 if (mapEl) { 7050 mapEl.value = [pb.ne_lng, pb.ne_lat, pb.sw_lng, pb.sw_lat, zoom].join('|'); 7051 } 7052 } catch(_e) {} 7053 // Move the map and trigger search via moveend 7054 if (typeof window.MapLeaflet !== 'undefined' && window.MapLeaflet.fitMapBoundsSafely) { 7055 // User explicitly pressed "ТÑÑÑи" with pending location bounds. 7056 // Signal autocomplete selection so fitMapBoundsSafely does NOT 7057 // skip the fit even when user previously dragged the map. 7058 try { window.__mirelaAutocompleteSelection = true; } catch(_e) {} 7059 try { window.__forceHomeAutoFitOnce = true; } catch(_e) {} 7060 try { 7061 window.MapLeaflet.fitMapBoundsSafely({ bounds: { ...pb, zoom: zoom } }); 7062 } catch(_e) {} 7063 try { setTimeout(function(){ window.__mirelaAutocompleteSelection = false; }, 2000); } catch(_e) {} 7064 return; 7065 } 7066 } 7067 } catch(_e) {} 7068 // No pending bounds â fall back to manual button 7069 const manualBtn = document.querySelector('.js-map-manual'); 7070 if (manualBtn) { 7071 try { manualBtn.click(); } catch(_e) {} 7072 } 7073 } 7074 7075 triggerManualSearch() { 7076 try { if (window.__disableAjaxList) { 7077 const form = document.querySelector('.js-search-form'); 7078 if (form) { 7079 try { if ((form.method||'').toLowerCase() !== 'get') { form.method = 'get'; } } catch(_e){} 7080 try { var act = form.getAttribute('action'); if (!act || act === 'javascript:void(0)') { form.setAttribute('action', window.location.pathname); } } catch(_e){} 7081 if (makeSearchQuery('submit', { form: form, force: true }) === false) { 7082 if (typeof form.requestSubmit === 'function') { form.requestSubmit(); } 7083 else { form.submit(); } 7084 } 7085 } 7086 return; 7087 } } catch(_e) {} 7088 7089 // List page: handle location-based searches properly. 7090 try { 7091 if (isListPageShell()) { 7092 const form = document.querySelector('.js-search-form'); 7093 if (form) {
7094 const listRuntime = window.MirelaSearch && window.MirelaSearch.listRuntime 7095 ? window.MirelaSearch.listRuntime 7096 : null; 7097 try { 7098 const shared = window.AdvancedSearchShared || null; 7099 if (shared && typeof shared.syncDefaultAdvancedPanelStateToForm === 'function') { 7100 shared.syncDefaultAdvancedPanelStateToForm({ 7101 panel: document.querySelector('.wd-search-form'), 7102 form: form 7103 }); 7104 } else if (shared && typeof shared.syncAdvancedPanelStateToForm === 'function') { 7105 shared.syncAdvancedPanelStateToForm({ 7106 panel: document.querySelector('.wd-search-form'), 7107 form: form 7108 }); 7109 } 7110 } catch (_eSyncPanel) {} 7111 const getVal = (sel) => { 7112 try { return String((form.querySelector(sel)?.value || '')).trim(); } catch(_e){ return ''; } 7113 }; 7114 const hasDistrict = getVal('[name="district_id"]') !== '' || form.querySelectorAll('input[name="district_id[]"]').length > 0; 7115 const hasGroup = getVal('[name="district_group_id"]') !== '' || form.querySelectorAll('input[name="district_group_id[]"]').length > 0; 7116 const hasStreet = getVal('[name="street_id"]') !== ''; 7117 const hasCityId = getVal('[name="city_id"]') !== ''; 7118 const hasExplicitLocation = hasDistrict || hasGroup || hasStreet || hasCityId; 7119 7120 if (listRuntime && typeof listRuntime.makeRequest === 'function') { 7121 try { 7122 listRuntime.makeRequest({ 7123 reason: 'search-autocomplete.triggerManualSearch', 7124 requestMode: hasExplicitLocation ? 'location' : undefined, 7125 form: form, 7126 force: true, 7127 append: false, 7128 resetPaging: true, 7129 explicitLocation: hasExplicitLocation 7130 }); 7131 return; 7132 } catch (_eListRuntime) {} 7133 } 7134 7135 // Polygon-based filter (district/group/street): use loadOffers path 7136 if (hasDistrict || hasGroup || hasStreet) { 7137 // Clear stale map bounds â polygon search doesn't use viewport 7138 try { 7139 const mapEl = form.querySelector('input[name="map"]'); 7140 if (mapEl) { mapEl.value = ''; } 7141 const mapEl2 = form.querySelector('.js-location-map'); 7142 if (mapEl2) { mapEl2.value = ''; } 7143 } catch(_e) {} 7144 // Strip stale ?map=/?map_manual= from URL so prepareListFormMapForRequest 7145 // does not re-inject Leaflet viewport bounds via the hasUrlMap branch. 7146 try { 7147 const _u = new URL(window.location.href); 7148 if (_u.searchParams.has('map') || _u.searchParams.has('map_manual')) { 7149 _u.searchParams.delete('map'); 7150 _u.searchParams.delete('map_manual'); 7151 history.replaceState(null, '', _u.pathname + (_u.search || '') + (_u.hash || '')); 7152 } 7153 } catch(_eStripMapUrl) {} 7154 // Exit bounds mode and force map to fit response bounds 7155 try { window.__listBoundsMode = false; } catch(_e) {} 7156 try { window.__landedWithMap = false; } catch(_e) {} 7157 try { 7158 if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapList === 'function') { 7159 window.MirelaSearch.applyUserMovedMapList(false); 7160 } else { 7161 window.__mirelaUserMovedMap = false; 7162 } 7163 } catch (_e) {} 7164 try { window.__mapManualNext = false; } catch(_e) {} 7165 try { window.__listForceFitOnce = true; } catch(_e) {} 7166 try { window.__listLocationSelectionFitNext = true; } catch(_e) {} 7167 // Consume pending bounds (server returns correct ones) 7168 try { 7169 window.__pendingLocationBounds = null; 7170 window.__pendingLocationBoundsTs = 0; 7171 } catch(_e) {} 7172 const triggerEl = 7173 form.querySelector('input[name="district_id"]') || 7174 form.querySelector('input[name="district_group_id"]') || 7175 form.querySelector('input[name="street_id"]') || 7176 form.querySelector('input[name="type"]'); 7177 if (triggerEl) { 7178 try { triggerEl.dispatchEvent(new Event('change', { bubbles: true })); } catch(_e) {} 7179 return; 7180 } 7181 } 7182 7183 // If the user selected a location (city/district) from autocomplete: 7184 // Clear stale map bounds, exit bounds mode, force-fit after response, 7185 // and use loadOffers with the city_id. The server returns the correct 7186 // bounds in the response, and forceFitOnce makes the map move there. 7187 if (hasCityId && !hasDistrict && !hasGroup && !hasStreet) { 7188 // Clear stale map bounds so loadOffers doesn't use old viewport 7189 try { 7190 const mapEl = form.querySelector('input[name="map"]'); 7191 if (mapEl) { mapEl.value = ''; } 7192 const mapEl2 = form.querySelector('.js-location-map'); 7193 if (mapEl2) { mapEl2.value = ''; } 7194 } catch(_e) {} 7195 // Strip stale ?map=/?map_manual= from URL so prepareListFormMapForRequest 7196 // does not re-inject Leaflet viewport bounds via the hasUrlMap branch. 7197 try { 7198 const _u = new URL(window.location.href); 7199 if (_u.searchParams.has('map') || _u.searchParams.has('map_manual')) { 7200 _u.searchParams.delete('map'); 7201 _u.searchParams.delete('map_manual'); 7202 history.replaceState(null, '', _u.pathname + (_u.search || '') + (_u.hash || '')); 7203 } 7204 } catch(_eStripMapUrl) {} 7205 // Exit bounds mode â we're now searching by city, not by viewport 7206 try { window.__listBoundsMode = false; } catch(_e) {} 7207 try { window.__landedWithMap = false; } catch(_e) {} 7208 try { 7209 if (window.MirelaSearch && typeof window.MirelaSearch.applyUserMovedMapList === 'function') { 7210 window.MirelaSearch.applyUserMovedMapList(false); 7211 } else { 7212 window.__mirelaUserMovedMap = false; 7213 } 7214 } catch (_e) {} 7215 try { window.__mapManualNext = false; } catch(_e) {} 7216 // Force map to fit to response bounds (even if auto-search is on) 7217 try { window.__listForceFitOnce = true; } catch(_e) {} 7218 try { window.__listLocationSelectionFitNext = true; } catch(_e) {} 7219 // Consume pending bounds (no longer needed, server will provide correct ones) 7220 try { 7221 window.__pendingLocationBounds = null; 7222 window.__pendingLocationBoundsTs = 0; 7223 } catch(_e) {} 7224 try { 7225 if (listRuntime && typeof listRu
7225ntime.makeRequest === 'function') { 7226 listRuntime.makeRequest({ 7227 reason: 'search-autocomplete.locationSelection.cityOnly', 7228 requestMode: 'location', 7229 form: form, 7230 force: true, 7231 append: false, 7232 resetPaging: true, 7233 explicitLocation: true 7234 }); 7235 return; 7236 } 7237 } catch(_e) {} 7238 } 7239 } 7240 } 7241 } catch(_e) {} 7242 7243 try { 7244 if (isListPageShell()) { 7245 const form = document.querySelector('.js-search-form'); 7246 const listRuntime = window.MirelaSearch && window.MirelaSearch.listRuntime 7247 ? window.MirelaSearch.listRuntime 7248 : null; 7249 if (form && listRuntime && typeof listRuntime.makeRequest === 'function') { 7250 listRuntime.makeRequest({ 7251 reason: 'search-autocomplete.listFallbackRequest', 7252 requestMode: 'location', 7253 form: form, 7254 force: true, 7255 append: false, 7256 resetPaging: true 7257 }); 7258 return; 7259 } 7260 } 7261 } catch (_eListFallback) {} 7262 7263 const btn = document.querySelector('.js-map-manual'); 7264 if (btn) { 7265 try { 7266 btn.dispatchEvent(new MouseEvent('click', { bubbles: false, cancelable: true })); 7267 } catch (err) {} 7268 return; 7269 } 7270 7271 // Fallback: no map/manual button present â build SEO route and redirect with current filters 7272 try { 7273 const form = document.querySelector('.js-search-form'); 7274 if (!form) return; 7275 7276 const msFb = window.MirelaSearch; 7277 if (msFb && typeof msFb.tryOffersSearchServerListNavigationFromForm === 'function') { 7278 const navFb = msFb.tryOffersSearchServerListNavigationFromForm(form); 7279 if (navFb && navFb.handled) { 7280 if (!navFb.sync && navFb.promise && typeof navFb.promise.catch === 'function') { 7281 navFb.promise.catch(function () {}); 7282 } 7283 return; 7284 } 7285 } 7286 } catch(_e) {} 7287 } 7288 7289 // Public methods 7290 destroy() { 7291 if (this.isInitialized) { 7292 try { this.$inputs && this.$inputs.off(); } catch(_e) {} 7293 try { $(document).off('.searchAutocomplete'); } catch(_e) {} 7294 this.isInitialized = false; 7295 } 7296 } 7297 7298 refresh() { 7299 this.destroy(); 7300 this.init(); 7301 } 7302 7303 _removeHistorySuggestionItem(itemEl) { 7304 try { this._setActiveContextFromSuggestionEl(itemEl); } catch (_e) {} 7305 const $item = $(itemEl).closest('.suggestion-item'); 7306 if (!$item.length) { return; } 7307 this._removeFromHistory($item); 7308 $item.remove(); 7309 this._collapseSuggestionsIfEmpty(); 7310 } 7311 7312 _collapseSuggestionsIfEmpty() { 7313 try { 7314 if (this.$suggestions && !this.$suggestions.find('.suggestion-item').length) { 7315 this._collapseSuggestions(this.$suggestions); 7316 } 7317 } catch (_e) {} 7318 } 7319 7320 // Remove a single suggestion from history (localStorage) 7321 _removeFromHistory($item) { 7322 try { 7323 const raw = $item.attr('data-value') || ''; 7324 if (!raw) return; 7325 let val = null; 7326 try { val = JSON.parse(raw.replace(/'/g, "'")); } catch(_e) {} 7327 if (!val) return; 7328 const sig = acValueSig(val); 7329 [HISTORY.KEY_AUTOCOMPLETE_A, HISTORY.KEY_AUTOCOMPLETE].forEac
7329h(key => { 7330 try { 7331 const arr = HISTORY.get(key); 7332 const filtered = arr.filter(el => { 7333 try { return acValueSig(el && el.value ? el.value : el) !== sig; } catch(_e) { return true; } 7334 }); 7335 if (filtered.length !== arr.length) { 7336 window.localStorage.setItem(key, JSON.stringify(filtered)); 7337 } 7338 } catch(_e) {} 7339 }); 7340 } catch(_e) {} 7341 } 7342 7343 // Show recent-history panel (used by input click toggle). No history â no-op (do not clear AJAX suggestions). 7344 maybeShowRecentHistory() { 7345 try { 7346 const items = this.getRecentHistoryItems(); 7347 if (items.length) { 7348 this.displaySuggestions(items); 7349 this.$suggestions.show(); 7350 } 7351 } catch(_e) {} 7352 } 7353 7354 getRecentHistoryItems() { 7355 const limit = this.options.historyLimits.acA || 9; 7356 const uniq = HISTORY.recentAutocompleteItems(limit); 7357 if (!uniq.length) { return []; } 7358 // Map to suggestion items format expected by displaySuggestions 7359 return uniq.map(it => { 7360 const v = Object.assign({}, acNormalizeValue(it.value || {})); 7361 try { v.__fromHistory = true; } catch(_e) {} 7362 return { label: it.label || it.typeahead || it.suggestion || '', value: v }; 7363 }); 7364 } 7365 } 7366 7367 // Initialize when DOM is ready 7368 $(document).ready(function() { 7369 // Initialize for all search inputs. 7370 // On homepage, the instance runs in "suggestions-only" mode (see class init()) to avoid 7371 // double-binding selection handlers with `public/js/pages/home.js`. 7372 // 7373 // Include this file only once per page (via `search-bar-scripts` is enough). A second 7374 // <script> tag would hit this guard and do nothing â it does NOT merge different page 7375 // behaviors. Home vs agents vs list differ via the server page shell, not via 7376 // loading autocomplete twice. 7377 try { 7378 if (window.searchAutocomplete && window.searchAutocomplete.isInitialized === true) { 7379 return; 7380 } 7381 window.searchAutocomplete = new SearchAutocomplete(); 7382 } catch (err) { 7383 // Ðко autocomplete Ñ Ð²ÑÑли гÑеÑка, не ÑÑпи дÑÑг JS 7384 } 7385 }); 7386 7387 // Export for global access 7388 window.SearchAutocomplete = SearchAutocomplete; 7389 7390})(jQuery); 7391 7392// ---- END: js/search-autocomplete.js ---- 7393 7394// ---- START: js/components/last-list-url-service.js ---- 7395/* Last-list context helper: SSR first, sessionStorage mirror second. */ 7396(function (window, document) { 7397 'use strict'; 7398 7399 var root = window.MirelaSearch = window.MirelaSearch || {}; 7400 var api = root.lastListUrlService = root.lastListUrlService || {}; 7401 7402 var DEAL_LIST_RE = /\/(?:prodazhbi|naemi|for-sale|for-rent|prodazha|arenda|verkauf|miete|kupuvachi|naemateli)(\/|\?|#|$)/i; 7403 var OFFER_LIKE_RE = /\/(?:prodazhbi|naemi|for-sale|for-rent|prodazha|arenda|verkauf|miete)\/[^/]+\/[^/]+-(\d{5,})$/i; 7404 7405 function trim(value) { 7406 return String(value == null ? '' : value).trim(); 7407 } 7408 7409 function writeSessionMirror(urlValue, labelValue) { 7410 try { 7411 if (typeof sessionStorage === 'undefined' || !sessionStorage) return; 7412 sessionStorage.setItem('__mirela_last_list_url', String(urlValue || '')); 7413 sessionStorage.setItem('__mirela_last_list_label', String(labelValue || '')); 7414 } catch (_e) {} 7415 } 7416 7417 /** 7418 * Normalize to a relative path + query suitable for a last-list mirror. 7419 * @returns {string} '' if invalid 7420 */ 7421 function normalizeLastListUrl(url, options) { 7422 var o = options || {}; 7423 try { 7424 var s = trim(url); 7425 if (!s) return ''; 7426 if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) { 7427 try { 7428 var u = new URL(s); 7429 s = (u.pathname || '') + (u.search || ''); 7430 } catch (_e) { 7431 return ''; 7432 } 7433 } 7434 if (!s || s[0] !== '/') return ''; 7435 if (o.skipListValidation) return s;
7436 return isPlausibleListRelativeUrl(s) ? s : ''; 7437 } catch (_e2) { 7438 return ''; 7439 } 7440 } 7441 7442 function isPlausibleListRelativeUrl(rel) { 7443 var s = trim(rel); 7444 if (!s || s[0] !== '/') return false; 7445 if (/(^|\/)offer\/\d+$/i.test(s)) return false; 7446 if (OFFER_LIKE_RE.test(s)) return false; 7447 try { 7448 var agentUtils = root.agentPathUtils || null; 7449 if (agentUtils && typeof agentUtils.isAgentListingPath === 'function' && agentUtils.isAgentListingPath(s)) { 7450 return true; 7451 } 7452 } catch (_eAgent) {} 7453 return DEAL_LIST_RE.test(s); 7454 } 7455 7456 function readLabelFromDom() { 7457 try { 7458 var inp = 7459 document.querySelector('.js-search-form .js-location-dd-input') || 7460 document.querySelector('.js-search-form .js-search-input'); 7461 var label = inp && inp.value != null ? trim(inp.value) : ''; 7462 return label; 7463 } catch (_e) { 7464 return ''; 7465 } 7466 } 7467 7468 /** 7469 * Drop one-shot query params from a list rel before persisting to session/offer-entry state. 7470 * Keeps ?page=N so list â offer â âСпиÑÑкâ returns to the same results page. 7471 * Strips focus_offer_id (re-added on back navigation) and restore_scroll (one-shot). 7472 * 7473 * IMPORTANT: do not use URL/URLSearchParams to rewrite the query â they re-encode values and break 7474 * the `map=` param (pipes, bounds order, length). Strip named segments only, preserving bytes. 7475 */ 7476 function normalizePersistentListRel(rel) { 7477 try { 7478 var s = trim(rel); 7479 if (!s) return s; 7480 if (/^https?:\/\//i.test(s)) { 7481 try { 7482 var abs = new URL(s); 7483 s = (abs.pathname || '') + (abs.search || '') + (abs.hash || ''); 7484 } catch (_a) { 7485 return rel; 7486 } 7487 } 7488 var q = s.indexOf('?'); 7489 if (q === -1) return s; 7490 var path = s.slice(0, q); 7491 var rest = s.slice(q + 1); 7492 var hash = ''; 7493 var h = rest.indexOf('#'); 7494 if (h !== -1) { 7495 hash = rest.slice(h); 7496 rest = rest.slice(0, h); 7497 } 7498 if (!rest) return path + hash; 7499 var segments = rest.split('&'); 7500 var kept = []; 7501 for (var i = 0; i < segments.length; i++) { 7502 var seg = segments[i]; 7503 if (!seg) continue; 7504 var eq = seg.indexOf('='); 7505 var rawKey = eq === -1 ? seg : seg.slice(0, eq); 7506 var key = rawKey; 7507 try { 7508 key = decodeURIComponent(String(rawKey).replace(/\+/g, ' ')); 7509 } catch (_k) {} 7510 var br = key.indexOf('['); 7511 var base = br === -1 ? key : key.slice(0, br); 7512 if (base === 'focus_offer_id' || base === 'restore_scroll') { 7513 continue; 7514 } 7515 kept.push(seg); 7516 } 7517 var qs = kept.join('&'); 7518 return path + (qs ? '?' + qs : '') + hash; 7519 } catch (_e) { 7520 return rel; 7521 } 7522 } 7523 7524 /** @deprecated use normalizePersistentListRel */ 7525 function stripFocusOfferIdFromRelativeUrl(rel) { 7526 return normalizePersistentListRel(rel); 7527 } 7528 7529 /** 7530 * After list scroll-to-offer: drop focus_offer_id from last-list and restore/entry session copies. 7531 */ 7532 function stripFocusOfferIdFromAllPersistedListUrls() { 7533 try { 7534 var ctx = readLastListContext(); 7535 if (ctx.url) { 7536 var nu = normalizePersistentListRel(ctx.url); 7537 if (nu !== ctx.url) { 7538 writeLastListContext({ 7539 url: nu, 7540 locationLabel: ctx.locationLabel, 7541 skipListValidation: true 7542 }); 7543 } 7544 } 7545 } catch (_e0) {} 7546 7547 try { 7548 var rs = sessionStorage.getItem('__mirela_restore_list_scroll'); 7549 if (rs) { 7550 var pr = JSON.parse(rs); 7551 if (pr && pr.listUrl) { 7552 var nlr = normalizePersistentListRel(String(pr.listUrl)); 7553 if (nlr !== pr.listUrl) { 7554 pr.listUrl = nlr; 7555 sessionStorage.setItem('__mirela_restore_list_scroll', JSON.stringify(pr)); 7556 } 7557 } 7558 } 7559 } catch (_e2) {} 7560 7561 try { 7562 var oe = sessionStorage.getItem('mirela_offer_entry_state_v1'); 7563 if (oe) { 7564 var po = JSON.parse(oe); 7565 if (po && po.listUrl) { 7566 var nlo = normalizePersistentListRel(String(po.listUrl)); 7567 if (nlo !== po.listUrl) { 7568 po.listUrl = nlo; 7569 sessionStorage.setItem('mirela_offer_entry_state_v1', JSON.stringify(po)); 7570 } 7571 } 7572 } 7573 } catch (_e3) {} 7574 7575 try { 7576 var ssl = sessionStorage.getItem('__mirela_last_list_url'); 7577 if (ssl) { 7578 var nls = normalizePersistentListRel(String(ssl)); 7579 if (nls !== ssl) sessionStorage.setItem('__mirela_last_list_url', nls); 7580 } 7581 } catch (_e4) {} 7582 } 7583 7584 /** @param {{ url?: string, locationLabel?: string, readLabelFromDom?: boolean, skipListValidation?: boolean }} payload */ 7585 function writeLastListContext(payload) { 7586 var p = payload || {}; 7587 var rel = normalizeLastListUrl(p.url, { skipListValidation: !!p.skipListValidation }); 7588 if (!rel) return false;
7589 rel = stripFocusOfferIdFromRelativeUrl(rel); 7590 7591 var label = trim(p.locationLabel); 7592 if (p.readLabelFromDom) { 7593 var domLabel = readLabelFromDom(); 7594 if (domLabel) label = domLabel; 7595 } 7596 7597 writeSessionMirror(rel, label); 7598 return true; 7599 } 7600 7601 function readLastListContext() { 7602 var url = ''; 7603 var locationLabel = ''; 7604 try { 7605 var nav = window.__MIRELA_SSR_LAST_LIST_NAV__; 7606 if (nav && typeof nav === 'object' && nav.rel) { 7607 url = String(nav.rel || ''); 7608 locationLabel = String(nav.label || ''); 7609 } 7610 } catch (_eSsr) {} 7611 try { 7612 if (!url && typeof sessionStorage !== 'undefined' && sessionStorage) { 7613 url = String(sessionStorage.getItem('__mirela_last_list_url') || ''); 7614 locationLabel = String(sessionStorage.getItem('__mirela_last_list_label') || ''); 7615 } 7616 } catch (_eSs) {} 7617 return { 7618 url: url, 7619 locationLabel: locationLabel 7620 }; 7621 } 7622 7623 function clearLastListUrlMirror() { 7624 try { 7625 if (typeof sessionStorage !== 'undefined' && sessionStorage) sessionStorage.removeItem('__mirela_last_list_url'); 7626 } catch (_e0) {} 7627 } 7628 7629 function clearLocationLabelMirror() { 7630 try { 7631 if (typeof sessionStorage !== 'undefined' && sessionStorage) sessionStorage.removeItem('__mirela_last_list_label'); 7632 } catch (_e0) {} 7633 } 7634 7635 /** 7636 * Drop offer list-back handoff markers when the user leaves preview for home/map shell. 7637 * Main session / last-list rel must not keep focus_offer_id for a later generic âСпиÑÑкâ. 7638 */ 7639 function clearEphemeralOfferHandoff() { 7640 try { 7641 if (typeof sessionStorage !== 'undefined' && sessionStorage) { 7642 sessionStorage.removeItem('__mirela_focus_offer_id'); 7643 } 7644 } catch (_e0) {} 7645 7646 try { 7647 if (typeof window.__mirelaClearOfferEntryState === 'function') { 7648 window.__mirelaClearOfferEntryState(); 7649 } 7650 } catch (_e1) {} 7651 7652 stripFocusOfferIdFromAllPersistedListUrls(); 7653 7654 try { 7655 var nav = window.MirelaSearch && window.MirelaSearch.nav; 7656 if (nav && typeof nav.setLastListContext === 'function') { 7657 var ctx = typeof nav.getLastListContext === 'function' ? nav.getLastListContext() : null; 7658 var listUrl = ctx && ctx.last_list_url ? String(ctx.last_list_url) : ''; 7659 if (listUrl) { 7660 nav.setLastListContext({ 7661 listUrl: listUrl, 7662 offerId: '', 7663 suppressFocusOfferId: true 7664 }); 7665 } 7666 } 7667 } catch (_e2) {} 7668 7669 try { 7670 var raw = sessionStorage.getItem('__mirela_restore_list_scroll'); 7671 if (raw) { 7672 var pr = JSON.parse(raw); 7673 if (pr && typeof pr === 'object') { 7674 delete pr.offerId; 7675 pr.source = 'cleared'; 7676 sessionStorage.setItem('__mirela_restore_list_scroll', JSON.stringify(pr)); 7677 } 7678 } 7679 } catch (_e3) {} 7680 } 7681 7682 api.normalizeLastListUrl = normalizeLastListUrl; 7683 api.isPlausibleListRelativeUrl = isPlausibleListRelativeUrl; 7684 api.writeLastListContext = writeLastListContext; 7685 api.readLastListContext = readLastListContext; 7686 api.clearLastListUrlMirror = clearLastListUrlMirror; 7687 api.clearLocationLabelMirror = clearLocationLabelMirror; 7688 api.normalizePersistentListRel = normalizePersistentListRel; 7689 api.stripFocusOfferIdFromRelativeUrl = stripFocusOfferIdFromRelativeUrl; 7690 api.stripFocusOfferIdFromAllPersistedListUrls = stripFocusOfferIdFromAllPersistedListUrls; 7691 api.clearEphemeralOfferHandoff = clearEphemeralOfferHandoff; 7692 window.__mirelaClearEphemeralOfferHandoff = clearEphemeralOfferHandoff; 7693})(window, document); 7694 7695// ---- END: js/components/last-list-url-service.js ---- 7696 7697// ---- START: js/components/back-target-resolver.js ---- 7698/* Single back-target resolver for list navigation (detail desktop + mobile). */ 7699(function (window, document) { 7700 'use strict'; 7701 7702 var root = window.MirelaSearch = window.MirelaSearch || {}; 7703 var api = root.backTargetResolver = root.backTargetResolver || {}; 7704 7705 var OFFER_ENTRY_SS_KEY = 'mirela_offer_entry_state_v1'; 7706 var NAV_BACK_SS_KEY = '__mirela_nav_back_target'; 7707 var SESSION_LIST_KEY = '__mirela_last_list_url'; 7708 var OFFER_ENTRY_TTL_MS = 1000 * 60 * 60; 7709 var NAV_BACK_TTL_MS = 30000; 7710 var OFFER_PREVIEW_RESTORE_MAX_MS = 10 * 60 * 1000; 7711 7712 var DEAL_SEGMENT_PATTERN = '(?:prodazhbi|naemi|for-sale|for-rent|prodazha|arenda|verkauf|miete|kupuvachi|naemateli)'; 7713 7714 function trim(value) { 7715 return String(value == null ? '' : value).trim(); 7716 } 7717 7718 function safeJsonParse(s) { 7719 try { 7720 return JSON.parse(String(s || '')); 7721 } catch (_e) { 7722 return null; 7723 } 7724 } 7725 7726 function isLocaleSegment(seg) { 7727 var s = String(seg || '').toLowerCase(); 7728 return s === 'en' || s === 'ru' || s === 'de' || s === 'bg'; 7729 } 7730 7731 function detectLocaleFromPath(pathname) { 7732 try { 7733 if (root.routeUtils && typeof root.routeUtils.detectLocaleFromPath === 'function') { 7734 return String(root.routeUtils.detectLocaleFromPath(pathname || '') || 'bg').toLowerCase() || 'bg'; 7735 } 7736 } catch (_e) {} 7737 try { 7738 var parts = String(pathname || '') 7739 .split('/') 7740 .filter(Boolean); 7741 var first = parts[0] || ''; 7742 if (isLocaleSegment(first)) return first.toLowerCase(); 7743 } catch (_e2) {} 7744 return 'bg'; 7745 } 7746 7747 function normalizeRelUrl(urlLike) { 7748 try { 7749 if (!urlLike) return ''; 7750 var s = trim(urlLike); 7751 if (!s) return ''; 7752 if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) { 7753 try { 7754 var u = new URL(s); 7755 s = (u.pathname || '') + (u.search || ''); 7756 } catch (_e) { 7757 return ''; 7758 } 7759 } 7760 if (!s || s[0] !== '/') return ''; 7761 s = collapseMapOnlyRelToDealPath(s); 7762 return s; 7763 } catch (_e2) { 7764 return ''; 7765 } 7766 } 7767 7768 function collapseMapOnlyRelToDealPath(rel) { 7769 try { 7770 var u = new URL(String(rel || ''), window.location.origin); 7771 var hasMap = trim(u.searchParams.get('map') || '') !== '' || trim(u.searchParams.get('map_bounds') || '') !== ''; 7772 if (!hasMap) return rel; 7773 var locationKeys = [ 7774 'city_id', 7775 'city_id[]', 7776 'district_id', 7777 'district_id[]', 7778 'district_group_id', 7779 'district_group_id[]', 7780 'street_id', 7781 'country_id', 7782 's' 7783 ]; 7784 for (var i = 0; i < locationKeys.length; i++) { 7785 if (trim(u.searchParams.get(locationKeys[i]) || '') !== '') { 7786 return rel; 7787 } 7788 } 7789 var parts = String(u.pathname || '').split('/').filter(
7789Boolean); 7790 if (!parts.length) return rel; 7791 var idx = isLocaleSegment(parts[0]) ? 1 : 0; 7792 var deal = parts[idx] || ''; 7793 if (!new RegExp('^' + DEAL_SEGMENT_PATTERN + '$', 'i').test(deal)) { 7794 return rel; 7795 } 7796 var localePrefix = idx === 1 ? '/' + parts[0] : ''; 7797 var collapsedPath = (localePrefix ? localePrefix + '/' : '/') + deal; 7798 if ((u.pathname || '') === collapsedPath) return rel; 7799 return collapsedPath + (u.search || '') + (u.hash || ''); 7800 } catch (_e) { 7801 return rel; 7802 } 7803 } 7804 7805 function toAbsolute(rel) { 7806 var r = normalizeRelUrl(rel); 7807 if (!r) return ''; 7808 try { 7809 return window.location.origin.replace(/\/$/, '') + r; 7810 } catch (_e) { 7811 return r; 7812 } 7813 } 7814 7815 /** 7816 * Locale handoff produced by nav.listBackAbsForCurrentUiLocale â must not treat like a normal list path. 7817 */ 7818 function isLocaleChangeHandoffRel(rel) { 7819 try { 7820 var pathOnly = String(normalizeRelUrl(rel) || '').split('?')[0] || ''; 7821 return /^\/change\/(?:en|ru|de|bg)(?:\/|$)/i.test(pathOnly); 7822 } catch (_e2) { 7823 return false; 7824 } 7825 } 7826 7827 function isListUrl(path) { 7828 try { 7829 var p = String(path || ''); 7830 if (!p) return false; 7831 if (/(^|\/)offer\/\d+$/i.test(p)) return false; 7832 // Offer slugs end with -{offerId} where offerId is 5+ digits. Shorter trailing digits are often 7833 // list location slugs (e.g. ...-mladost-2-3-4), not offer IDs â keep parity with Blade / last-list-url. 7834 if ( 7835 new RegExp('/' + DEAL_SEGMENT_PATTERN + '/[^/]+/[^/]+-\\d{5,}$', 'i').test(p) 7836 ) { 7837 return false; 7838 } 7839 return new RegExp('/' + DEAL_SEGMENT_PATTERN + '(/|\\?|#|$)', 'i').test(p); 7840 } catch (_e) { 7841 return false; 7842 } 7843 } 7844 7845 function isValidListRel(rel) { 7846 var n = normalizeRelUrl(rel); 7847 if (!n) return false; 7848 if (isLocaleChangeHandoffRel(n)) return true; 7849 try { 7850 var u = new URL(n, window.location.origin); 7851 return isListUrl(u.pathname || ''); 7852 } catch (_e) { 7853 return false; 7854 } 7855 } 7856 7857 function isAgentListingPath(pathname) { 7858 try { 7859 var agentUtils = root.agentPathUtils || null; 7860 if (agentUtils && typeof agentUtils.isAgentListingPath === 'function') { 7861 return !!agentUtils.isAgentListingPath(pathname || ''); 7862 } 7863 } catch (_e) {} 7864 return false; 7865 } 7866 7867 /** List-back targets: standard deal/demand lists plus agent SEO listing pages. */ 7868 function isListBackTargetRel(rel) { 7869 var n = normalizeRelUrl(rel); 7870 if (!n) return false; 7871 if (isLocaleChangeHandoffRel(n)) return true; 7872 try { 7873 var u = new URL(n, window.location.origin); 7874 if (isAgentListingPath(u.pathname || '')) return true; 7875 return isListUrl(u.pathname || ''); 7876 } catch (_e) { 7877 return false; 7878 } 7879 } 7880 7881 function getSvc() { 7882 try { 7883 return root.lastListUrlService || null; 7884 } catch (_e) { 7885 return null; 7886 } 7887 } 7888 7889 function svcValidRel(rel) { 7890 var svc = getSvc(); 7891 if (svc && typeof svc.isPlausibleListRelativeUrl === 'function') { 7892 return !!svc.isPlausibleListRelativeUrl(normalizeRelUrl(rel)); 7893 } 7894 return isListBackTargetRel(rel); 7895 } 7896 7897 function hasMapParam(rel) { 7898 try { 7899 var n = normalizeRelUrl(rel); 7900 if (!n) return false; 7901 var u = new URL(n, window.location.origin); 7902 return trim(u.searchParams.get('map') || '') !== ''; 7903 } catch (_e) { 7904 return false; 7905 } 7906 } 7907 7908 /** Demands list (kupuvachi / naemateli), not a demand detail SEO URL. */ 7909 function isDemandsListRel(relLike) { 7910 try { 7911 var n = normalizeRelUrl(relLike); 7912 if (!n) return false; 7913 var u = new URL(n, window.location.origin); 7914 var path = u.pathname || ''; 7915 if (!/\/(kupuvachi|naemateli)(\/|$)/i.test(path)) { 7916 return false; 7917 } 7918 return isListUrl(path); 7919 } catch (_e) { 7920 return false; 7921 } 7922 } 7923 7924 /** User is on a demand detail page (slug-id under kupuvachi/naemateli), not the demands index. */ 7925 function isDemandDetailBrowseContext() { 7926 try { 7927 var p = String(window.location.pathname || ''); 7928 if (!/\/(kupuvachi|naemateli)\//i.test(p)) { 7929 return false; 7930 } 7931 return !isListUrl(p); 7932 } catch (_e2) { 7933 return false; 7934 } 7935 } 7936 7937 function listPageNumberFromRel(rel) { 7938 try { 7939 var u = new URL(normalizeRelUrl(rel), window.location.origin); 7940 var p = parseInt(String(u.searchParams.get('page') || '1'), 10); 7941 return isFinite(p) && p > 0 ? p : 1; 7942 } catch (_e) { 7943 return 1; 7944 } 7945 } 7946 7947 function hasComplexNamespace(rel) { 7948 try { 7949 var p = String(normalizeRelUrl(rel) || '').toLowerCase(); 7950 return p.indexOf('/kompleks/') !== -1 || p.indexOf('/complex/') !== -1 || p.indexOf('/komplex/') !== -1; 7951 } catch (_e) { 7952 return false; 7953 } 7954 } 7955
7956 function hasStreetNamespace(rel) { 7957 try { 7958 var p = String(normalizeRelUrl(rel) || '').toLowerCase(); 7959 return p.indexOf('/ulitsa/') !== -1 || p.indexOf('/street/') !== -1 || p.indexOf('/strasse/') !== -1; 7960 } catch (_e) { 7961 return false; 7962 } 7963 } 7964 7965 function preferRicherListUrl(currentUrl, candidateUrl) { 7966 var cur = normalizeRelUrl(currentUrl); 7967 var next = normalizeRelUrl(candidateUrl); 7968 if (!next) return cur; 7969 7970 var nextHandoff = isLocaleChangeHandoffRel(next); 7971 var curHandoff = isLocaleChangeHandoffRel(cur); 7972 var demandCtx = isDemandDetailBrowseContext(); 7973 7974 if (!cur) { 7975 if (demandCtx && !nextHandoff && !isDemandsListRel(next)) { 7976 return ''; 7977 } 7978 if (nextHandoff || isListBackTargetRel(next)) return next; 7979 return cur; 7980 } 7981 7982 // Patched /change/{locale}?to=⦠wins over plain list rel (correct UI locale + encoded inner URL). 7983 if (nextHandoff && !curHandoff) return next; 7984 if (curHandoff && !nextHandoff) return cur; 7985 if (nextHandoff && curHandoff) { 7986 return next.length >= cur.length ? next : cur; 7987 } 7988 7989 if (!isListBackTargetRel(next)) return cur; 7990 if (!isListBackTargetRel(cur)) return next; 7991 7992 // Preview/list-back: when one candidate points to an explicit complex path and the other 7993 // points to a street path, prefer the complex URL. Runtime canonical state can occasionally 7994 // rebuild `/ulitsa/...` from the current offer context, while the remembered/Blade list URL 7995 // still carries the authoritative `/kompleks/...` path from the list page. 7996 var curComplex = hasComplexNamespace(cur); 7997 var nextComplex = hasComplexNamespace(next); 7998 var curStreet = hasStreetNamespace(cur); 7999 var nextStreet = hasStreetNamespace(next); 8000 if (curComplex && nextStreet && !nextComplex) return cur; 8001 if (nextComplex && curStreet && !curComplex) return next; 8002 8003 if (demandCtx) { 8004 var curD = isDemandsListRel(cur); 8005 var nextD = isDemandsListRel(next); 8006 if (curD && !nextD) return cur; 8007 if (!curD && nextD) return next; 8008 } 8009 8010 var curHasMap = hasMapParam(cur); 8011 var nextHasMap = hasMapParam(next); 8012 if (nextHasMap && !curHasMap) return next; 8013 if (curHasMap && !nextHasMap) return cur; 8014 8015 var curPage = listPageNumberFromRel(cur); 8016 var nextPage = listPageNumberFromRel(next); 8017 if (nextPage > curPage) return next; 8018 if (curPage > nextPage) return cur; 8019 8020 try { 8021 var uCur = new URL(cur, window.location.origin); 8022 var uNext = new URL(next, window.location.origin); 8023 var curLen = String(uCur.search || '').length; 8024 var nextLen = String(uNext.search || '').length; 8025 if (nextLen > curLen) return next; 8026 } catch (_e) {} 8027 8028 return cur; 8029 } 8030 8031 function buildBackUrl(listRel, offerId) { 8032 try { 8033 if (root.nav && typeof root.nav.buildBackUrl === 'function') { 8034 return String(root.nav.buildBackUrl(listRel, offerId) || ''); 8035 } 8036 } catch (_e) {} 8037 try { 8038 var rel0 = normalizeRelUrl(listRel); 8039 if (!rel0) return ''; 8040 var oid = trim(offerId); 8041 if (!oid) return rel0; 8042 var u = new URL(rel0, window.location.origin); 8043 if (!trim(u.searchParams.get('focus_offer_id') || '')) { 8044 u.searchParams.set('focus_offer_id', oid); 8045 } 8046 return (u.pathname || '') + (u.search || ''); 8047 } catch (_e2) { 8048 return normalizeRelUrl(listRel); 8049 } 8050 } 8051 8052 /** 8053 * Add focus_offer_id to the list URL, or inject it into the inner `to` of a /change/{locale}?to=⦠handoff. 8054 */ 8055 function mergeFocusIntoListBackRel(rel, offerId) { 8056 var oid = trim(offerId); 8057 var n = normalizeRelUrl(rel); 8058 if (!n) return ''; 8059 if (!oid) return n; 8060 if (isLocaleChangeHandoffRel(n)) { 8061 try { 8062 var u = new URL(n, window.location.origin); 8063 var rawTo = u.searchParams.get('to'); 8064 if (!rawTo) { 8065 return buildBackUrl(n, oid); 8066 } 8067 var innerPath = rawTo; 8068 try { 8069 innerPath = decodeURIComponent(rawTo.replace(/\+/g, '%20')); 8070 } catch (_eDec) {} 8071 var pathForUrl = innerPath; 8072 if (pathForUrl.indexOf('http://') !== 0 && pathForUrl.indexOf('https://') !== 0) { 8073 if (pathForUrl.charAt(0) !== '/') pathForUrl = '/' + pathForUrl; 8074 } 8075 var uInner = new URL(pathForUrl, window.location.origin); 8076 if (!trim(uInner.searchParams.get('focus_offer_id') || '')) { 8077 uInner.searchParams.set('focus_offer_id', oid); 8078 } 8079 var newInner = (uInner.pathname || '') + (uInner.search || ''); 8080 u.searchParams.set('to', newInner); 8081 return (u.pathname || '') + (u.search || ''); 8082 } catch (_e) { 8083 return buildBackUrl(n, oid); 8084 } 8085 } 8086 return buildBackUrl(n, oid); 8087 } 8088 8089 function getCurrentOfferIdFromLocation() { 8090 try { 8091 var p = String(window.location.pathname || ''); 8092 var m = p.match(/\/offer\/(\d+)(?:\/)?$/i); 8093 if (m && m[1]) return String(m[1]);
8094 var seg = p.split('/').filter(Boolean).pop() || ''; 8095 m = seg.match(/-(\d+)$/); 8096 if (m && m[1]) return String(m[1]); 8097 } catch (_e) {} 8098 return ''; 8099 } 8100 8101 /** 8102 * Session snapshot from the list â offer transition (not updated on in-offer âsimilarâ clicks). 8103 */ 8104 function readOfferEntryState() { 8105 try { 8106 if (typeof sessionStorage === 'undefined' || !sessionStorage) return null; 8107 var raw = sessionStorage.getItem(OFFER_ENTRY_SS_KEY); 8108 if (!raw) return null; 8109 var parsed = safeJsonParse(raw); 8110 if (!parsed || typeof parsed !== 'object') return null; 8111 var ts = parseInt(parsed.ts || '0', 10) || 0; 8112 if (!ts || Date.now() - ts > OFFER_ENTRY_TTL_MS) return null; 8113 var listUrl = normalizeRelUrl(parsed.listUrl || ''); 8114 if (!listUrl || !isListBackTargetRel(listUrl)) return null; 8115 return { listUrl: listUrl, offerId: trim(parsed.offerId || '') }; 8116 } catch (_e) { 8117 return null; 8118 } 8119 } 8120 8121 function readOfferEntryRel() { 8122 var st = readOfferEntryState(); 8123 return st && st.listUrl ? st.listUrl : ''; 8124 } 8125 8126 function readOfferEntryLock() { 8127 var st = readOfferEntryState(); 8128 if (!st || !st.listUrl) return null; 8129 return { 8130 listUrl: normalizeRelUrl(st.listUrl), 8131 offerId: trim(st.offerId || '') 8132 }; 8133 } 8134 8135 function readNavBackTargetRel() { 8136 try { 8137 if (typeof sessionStorage === 'undefined' || !sessionStorage) return ''; 8138 var raw = sessionStorage.getItem(NAV_BACK_SS_KEY); 8139 if (!raw) return ''; 8140 var parsed = safeJsonParse(raw); 8141 if (!parsed) return ''; 8142 var ts = parseInt(parsed.ts || '0', 10) || 0; 8143 if (!ts || Date.now() - ts > NAV_BACK_TTL_MS) return ''; 8144 var u = trim(parsed.url || ''); 8145 if (!u) return ''; 8146 return normalizeRelUrl(u); 8147 } catch (_e) { 8148 return ''; 8149 } 8150 } 8151 8152 function readSsrLastListRel() { 8153 try { 8154 var nav = window.__MIRELA_SSR_LAST_LIST_NAV__; 8155 if (!nav || typeof nav !== 'object') return ''; 8156 return normalizeRelUrl(nav.rel || ''); 8157 } catch (_e) { 8158 return ''; 8159 } 8160 } 8161 8162 function readLastListContextRel() { 8163 try { 8164 var svc = getSvc(); 8165 if (svc && typeof svc.readLastListContext === 'function') { 8166 var ctx = svc.readLastListContext(); 8167 var u = ctx && ctx.url ? trim(ctx.url) : ''; 8168 return normalizeRelUrl(u); 8169 } 8170 } catch (_e) {} 8171 return ''; 8172 } 8173 8174 function readSessionMirrorRel() { 8175 try { 8176 if (typeof sessionStorage === 'undefined' || !sessionStorage) return ''; 8177 return normalizeRelUrl(sessionStorage.getItem(SESSION_LIST_KEY) || ''); 8178 } catch (_e) { 8179 return ''; 8180 } 8181 } 8182 8183 function readReferrerRel() { 8184 try { 8185 var ref = trim(document.referrer || ''); 8186 if (!ref) return ''; 8187 var u = new URL(ref, window.location.origin); 8188 if (u.origin !== window.location.origin) return ''; 8189 return normalizeRelUrl((u.pathname || '') + (u.search || '')); 8190 } catch (_e) { 8191 return ''; 8192 } 8193 } 8194 8195 function defaultFallbackListRel(pageLocale) { 8196 var loc = trim(pageLocale) || detectLocaleFromPath(window.location.pathname || ''); 8197 var salesSlug = 8198 loc === 'en' ? 'for-sale' : loc === 'ru' ? 'prodazha' : 'prodazhbi'; 8199 if (loc === 'bg') return '/' + salesSlug; 8200 return '/' + loc + '/' + salesSlug; 8201 } 8202 8203 /** 8204 * Strict waterfall; saved context vs referrer merged with preferRicherListUrl. 8205 * @param {{ pageLocale?: string, offerId?: string, forOfferPreview?: boolean }} options 8206 * @returns {{ rel: string, abs: string, source: string }} 8207 */ 8208 function resolveBackTarget(options) { 8209 var o = options || {}; 8210 var pageLocale = trim(o.pageLocale) || detectLocaleFromPath(window.location.pathname || ''); 8211 var offerIdFromPage = trim(o.offerId) || getCurrentOfferIdFromLocation(); 8212 var entrySt = readOfferEntryState(); 8213 var focusOfferId = 8214 entrySt && trim(entrySt.offerId) ? trim(entrySt.offerId) : offerIdFromPage; 8215 8216 function withFocus(rel, source) { 8217 var r = normalizeRelUrl(rel); 8218 if (!r || !svcValidRel(r)) return null; 8219 if (!isListBackTargetRel(r)) return null; 8220 if (isDemandDetailBrowseContext() && !isDemandsListRel(r)) return null; 8221 var built = focusOfferId ? buildBackUrl(r, focusOfferId) : r; 8222 var b = normalizeRelUrl(built); 8223 if (!b || !isListBackTargetRel(b)) return null; 8224 return { rel: b, abs: toAbsolute(b), source: source }; 8225 } 8226 8227 var fromEntry = readOfferEntryRel(); 8228 if (fromEntry) { 8229 var a = withFocus(fromEntry, 'offer_entry'); 8230 if (a) return a; 8231 } 8232 8233 var fromNavBack = readNavBackTargetRel(); 8234 if (fromNavBack) { 8235 var b = withFocus(fromNavBack, 'nav_back_target'); 8236 if (b) return b; 8237 } 8238 8239 var refRelEarly = readReferrerRel(); 8240 if (refRelEarly) { 8241 try { 8242 var refPathEarly = new URL(refRelEarly, window.location.origin).pathname || ''; 8243 if (isAgentListingPath(refPathEarly)) { 8244 var agentRef = withFocus(refRelEarly, 'referrer_agent'); 8245 if (agentRef) return agentRef; 8246 } 8247 } catch (_eAgentRef) {} 8248 } 8249 8250 var fromSsrNav = readSsrLastListRel(); 8251 if (fromSsrNav) { 8252 var s = withFocus(fromSsrNav, 'ssr_session_nav'); 8253 if (s) return s; 8254 } 8255 8256 var contextRel = readLastListContextRel(); 8257 var refRel = readReferrerRel(); 8258 var merged = ''; 8259 if (contextRel && refRel && isListBackTargetRel(contextRel) && isListBackTargetRel(refRel)) { 8260 merged = preferRicherListUrl(contextRel, refRel); 8261 } else if (contextRel && isListBackTargetRel(contextRel)) { 8262 merged = contextRel; 8263 } else if (refRel && isListBackTargetRel(refRel)) { 8264 merged = refRel; 8265 } 8266 if (merged) { 8267 var d = withFocus(merged, 'context_or_referrer'); 8268 if (d) return d; 8269 } 8270 8271 var sessionRel = readSessionMirrorRel(); 8272 if (sessionRel) { 8273 var e = withFocus(sessionRel, 'session_mirror'); 8274 if (e) return e; 8275 } 8276 8277 var fb = defaultFallbackListRel(pageLocale); 8278 var f = withFocus(fb, 'fallback'); 8279 if (f) return f; 8280 return { rel: '', abs: '', source: 'none' }; 8281 } 8282 8283 function bladeToRel(bladeHref) { 8284 if (!bladeHref || bladeHref === '#') return ''; 8285 try { 8286 if (bladeHref.indexOf('http://') === 0 || bladeHref.indexOf('https://') === 0) { 8287 var u = new URL(bladeHref); 8288 if (u.origin !== window.location.origin) return ''; 8289 return normalizeRelUrl((u.pathname || '') + (u.search || '')); 8290 } 8291 } catch (_e) {} 8292 return normalizeRelUrl(bladeHref); 8293 } 8294 8295 /** 8296 * Fresh offer-preview list URL stamped before navigation (mobile / tests). 8297 */ 8298 function readOfferPreviewRestoreListRel() { 8299 try { 8300 if (typeof sessionStorage === 'undefined' || !sessionStorage) return ''; 8301 var raw = trim(sessionStorage.getItem('__mirela_restore_list_scroll') || ''); 8302 if (!raw) return ''; 8303 var parsed = safeJsonParse(raw); 8304 if (!parsed || typeof parsed !== 'object') return ''; 8305 var ts = parseInt(String(parsed.ts || '0'), 10) || 0; 8306 if (!ts || Date.now() - ts > OFFER_PREVIEW_RESTORE_MAX_MS) return ''; 8307 if (String(parsed.source || '').trim() !== 'offer_preview') return ''; 8308 var rel = normalizeRelUrl(parsed.listUrl || ''); 8309 if (!rel || !isValidListRel(rel)) return ''; 8310 return rel; 8311 } catch (_e2) { 8312 return ''; 8313 } 8314 } 8315 8316 function mergeNavContextListUrl(listUrl, offerId) { 8317 try { 8318 if (!root.nav || typeof root.nav.getLastListContext !== 'function') return listUrl; 8319 var ctx = root.nav.getLastListContext(); 8320 var ctxUrl = ctx && ctx.last_list_url ? trim(ctx.last_list_url) : ''; 8321 var ctxOid = ctx && ctx.last_offer_id != null ? String(ctx.last_offer_id).trim() : ''; 8322 var oid = trim(offerId); 8323 var sameOffer = !oid || (ctxOid && ctxOid === oid); 8324 if (ctxUrl && sameOffer) { 8325 var exactListUrl = normalizeRelUrl(listUrl); 8326 var navCtxListUrl = normalizeRelUrl(ctxUrl); 8327 if (isDemandDetailBrowseContext()) { 8328 if (isDemandsListRel(exactListUrl) && !isDemandsListRel(navCtxListUrl)) { 8329 return exactListUrl; 8330 } 8331 if (!isDemandsListRel(exactListUrl) && isDemandsListRel(navCtxListUrl)) { 8332 return navCtxListUrl; 8333 } 8334 } 8335 if ( 8336 exactListUrl && 8337 navCtxListUrl && 8338 !hasMapParam(exactListUrl) && 8339 hasMapParam(navCtxListUrl) 8340 ) { 8341 return exactListUrl; 8342 } 8343 return preferRicherListUrl(exactListUrl, navCtxListUrl); 8344 } 8345 } catch (_e) {} 8346 return listUrl; 8347 } 8348 8349 /** 8350 * Single entry point for desktop .js-list-back (subscribe) and mobile goToList(). 8351 * @param {{ mode?: 'subscribe'|'mobile', offerId?: string, pageLocale?: string, listBackHrefOrDataUrl?: string, fallbackRel?: string }} options 8352 * @returns {string} pathname+search with focus_offer_id (and inner to= when handoff) 8353 */ 8354 function composeListBackRel(options) { 8355 var o = options || {}; 8356 var mode = trim(o.mode) || 'subscribe'; 8357 var offerIdFromPage = trim(o.offerId) || getCurrentOfferIdFromLocation(); 8358 var entryStForFocus = readOfferEntryState(); 8359 var entryLock = readOfferEntryLock(); 8360 var focusOfferId = 8361 entryStForFocus && trim(entryStForFocus.offerId) 8362 ? trim(entryStForFocus.offerId) 8363 : offerIdFromPage; 8364 var pageLocale = trim(o.pageLocale) || detectLocaleFromPath(window.location.pathname || ''); 8365 var listUrl = null; 8366 8367 var exact = readOfferEntryRel(); 8368 if (exact && isListBackTargetRel(normalizeRelUrl(exact))) { 8369 if (!isDemandDetailBrowseContext() || isDemandsListRel(exact)) { 8370 listUrl = normalizeRelUrl(exact); 8371 } 8372 } 8373 8374 if (mode === 'subscribe') { 8375 var hasExactEntryListUrl = !!(entryLock && trim(entryLock.listUrl)); 8376 if (hasExactEntryListUrl) { 8377 listUrl = normalizeRelUrl(entryLock.listUrl); 8378 if (entryLock.offerId) { 8379 focusOfferId = entryLock.offerId; 8380 } 8381 } else { 8382 listUrl = mergeNavContextListUrl(listUrl, focusOfferId); 8383 if (o.listBackHrefOrDataUrl) { 8384 var d0 = bladeToRel(trim(o.listBackHrefOrDataUrl)); 8385 if (d0) listUrl = preferRicherListUrl(listUrl, d0); 8386 } 8387 var resS = resolveBackTarget({ offerId: offerIdFromPage, pageLocale: pageLocale }); 8388 if (resS && resS.rel && trim(resS.rel)) { 8389 listUrl = preferRicherListUrl(listUrl, resS.rel); 8390 } 8391 } 8392 if (!listUrl) { 8393 var ctxS = readLastListContextRel(); 8394 if (ctxS && (!isDemandDetailBrowseContext() || isDemandsListRel(ctxS))) { 8395 listUrl = normalizeRelUrl(ctxS); 8396 } 8397 } 8398 } else { 8399 // mobile: match legacy goToList â no early nav;
8399 preview / resolver short-circuit before context/page/referrer waterfall 8400 var hasLock = !!(listUrl && trim(listUrl)); 8401 if (!hasLock) { 8402 var prev = readOfferPreviewRestoreListRel(); 8403 if (prev) { 8404 listUrl = prev; 8405 hasLock = true; 8406 } 8407 } 8408 if (!hasLock) { 8409 var resM = resolveBackTarget({ offerId: offerIdFromPage, pageLocale: pageLocale }); 8410 if (resM && resM.rel && trim(resM.rel)) { 8411 listUrl = normalizeRelUrl(resM.rel); 8412 hasLock = true; 8413 } 8414 } 8415 if (!hasLock) { 8416 listUrl = mergeNavContextListUrl(listUrl, focusOfferId); 8417 if (o.listBackHrefOrDataUrl) { 8418 var d1 = bladeToRel(trim(o.listBackHrefOrDataUrl)); 8419 if (d1) listUrl = preferRicherListUrl(listUrl, d1); 8420 } 8421 var refM = readReferrerRel(); 8422 if (refM && isListBackTargetRel(refM)) listUrl = preferRicherListUrl(listUrl, refM); 8423 var sessM = readSessionMirrorRel(); 8424 if (sessM && isListBackTargetRel(sessM)) listUrl = preferRicherListUrl(listUrl, sessM); 8425 var ctxM = readLastListContextRel(); 8426 if (ctxM && isListBackTargetRel(ctxM)) { 8427 if (!isDemandDetailBrowseContext() || isDemandsListRel(ctxM)) { 8428 listUrl = preferRicherListUrl(listUrl, ctxM); 8429 } 8430 } 8431 } 8432 } 8433 8434 if (isDemandDetailBrowseContext() && listUrl && !isDemandsListRel(listUrl)) { 8435 var bladeSan = o.listBackHrefOrDataUrl ? bladeToRel(trim(o.listBackHrefOrDataUrl)) : ''; 8436 if (isDemandsListRel(bladeSan)) { 8437 listUrl = bladeSan; 8438 } else { 8439 var exSan = readOfferEntryRel(); 8440 if (isDemandsListRel(exSan)) { 8441 listUrl = normalizeRelUrl(exSan); 8442 } else { 8443 var refSan = readReferrerRel(); 8444 if (isDemandsListRel(refSan)) { 8445 listUrl = refSan; 8446 } 8447 } 8448 } 8449 } 8450 8451 if (!listUrl || !trim(listUrl)) { 8452 var fb = trim(o.fallbackRel); 8453 if (fb) listUrl = normalizeRelUrl(fb); 8454 } 8455 8456 if (o.suppressFocusOfferId) { 8457 return listUrl || ''; 8458 } 8459 8460 if (!focusOfferId) return listUrl || ''; 8461 return mergeFocusIntoListBackRel(listUrl || '', focusOfferId) || listUrl || ''; 8462 } 8463 8464 /** 8465 * Patch .js-list-back anchors when runtime target is valid and richer than Blade. 8466 * After a header locale switch, Blade/session often has the localized list path + filters but 8467 * deliberately omits focus_offer_id; preferRicherListUrl() alone would keep that href and skip 8468 * patching â merge focus onto the chosen base URL so "СпиÑÑк" still returns to the same offer. 8469 */ 8470 function patchJsListBackAnchors() { 8471 try { 8472 var anchors = document.querySelectorAll('a.js-list-back'); 8473 if (!anchors || !anchors.length) return; 8474 8475 var offerIdFromPage = getCurrentOfferIdFromLocation(); 8476 var entryPatch = readOfferEntryState(); 8477 var entryLock = readOfferEntryLock(); 8478 var focusOfferId = 8479 entryPatch && trim(entryPatch.offerId) ? trim(entryPatch.offerId) : offerIdFromPage; 8480 var pageLocale = detectLocaleFromPath(window.location.pathname || ''); 8481 var resolved = entryLock && entryLock.listUrl 8482 ? { 8483 rel: normalizeRelUrl(entryLock.listUrl), 8484 abs: toAbsolute(normalizeRelUrl(entryLock.listUrl)), 8485 source: 'offer_entry' 8486 } 8487 : resolveBackTarget({ 8488 pageLocale: pageLocale, 8489 offerId: offerIdFromPage, 8490 forOfferPreview: true 8491 }); 8492 8493 if (!resolved || !resolved.rel || !resolved.abs) return; 8494 if (entryLock && entryLock.offerId) { 8495 focusOfferId = entryLock.offerId; 8496 } 8497 8498 for (var i = 0; i < anchors.length; i++) { 8499 var a = anchors[i]; 8500 var bladeHref = trim(a.getAttribute('data-list-url') || a.getAttribute('href') || ''); 8501 var bladeRel = bladeToRel(bladeHref); 8502 8503 var mergedBase = normalizeRelUrl(resolved.rel); 8504 if (!(entryLock && entryLock.listUrl) && bladeRel && isListBackTargetRel(bladeRel)) { 8505 // Keep the exact Blade list-back URL when it already points to an explicit complex path. 8506 // Runtime state can occasionally rebuild the same target as `/ulitsa/...` from current preview
8507 // state, but the anchor already carries the authoritative list URL from the list page. 8508 if (hasComplexNamespace(bladeRel) && hasStreetNamespace(resolved.rel)) { 8509 mergedBase = normalizeRelUrl(bladeRel); 8510 } else { 8511 mergedBase = preferRicherListUrl(bladeRel, resolved.rel); 8512 } 8513 } 8514 if (!mergedBase || !isListBackTargetRel(normalizeRelUrl(mergedBase))) { 8515 mergedBase = normalizeRelUrl(resolved.rel); 8516 } 8517 8518 var finalRel = focusOfferId 8519 ? mergeFocusIntoListBackRel(mergedBase, focusOfferId) 8520 : normalizeRelUrl(mergedBase); 8521 if (!finalRel || !trim(finalRel)) continue; 8522 8523 var finalAbs = toAbsolute(finalRel); 8524 try { 8525 if (root.nav && typeof root.nav.listBackAbsForCurrentUiLocale === 'function') { 8526 var wrapped = root.nav.listBackAbsForCurrentUiLocale(finalRel); 8527 if (wrapped) finalAbs = wrapped; 8528 } 8529 } catch (_eN) {} 8530 8531 var same = false; 8532 try { 8533 var uCur = new URL(bladeRel || '/', window.location.origin); 8534 var uNew = new URL(finalAbs); 8535 same = 8536 String(uCur.pathname || '') === String(uNew.pathname || '') && 8537 String(uCur.search || '') === String(uNew.search || ''); 8538 } catch (_eEq) {} 8539 if (same) continue; 8540 8541 a.setAttribute('href', finalAbs); 8542 a.setAttribute('data-list-url', finalAbs); 8543 try { 8544 a.classList.remove('js-list-back-pending'); 8545 a.removeAttribute('aria-hidden'); 8546 a.style.removeProperty('display'); 8547 } catch (_eShow) {} 8548 } 8549 } catch (_e) {} 8550 } 8551 8552 api.detectLocaleFromPath = detectLocaleFromPath; 8553 api.normalizeRelUrl = normalizeRelUrl; 8554 api.preferRicherListUrl = preferRicherListUrl; 8555 api.isValidListRel = isValidListRel; 8556 api.isListBackTargetRel = isListBackTargetRel; 8557 api.isAgentListingPath = isAgentListingPath; 8558 api.isLocaleChangeHandoffRel = isLocaleChangeHandoffRel; 8559 api.mergeFocusIntoListBackRel = mergeFocusIntoListBackRel; 8560 api.bladeToRel = bladeToRel; 8561 api.resolveBackTarget = resolveBackTarget; 8562 api.composeListBackRel = composeListBackRel; 8563 api.patchJsListBackAnchors = patchJsListBackAnchors; 8564 api.getCurrentOfferIdFromLocation = getCurrentOfferIdFromLocation; 8565 api.isDemandsListRel = isDemandsListRel; 8566 api.isDemandDetailBrowseContext = isDemandDetailBrowseContext; 8567})(window, document); 8568 8569// ---- END: js/components/back-target-resolver.js ---- 8570 8571// ---- START: js/components/search-last-list-url.js ---- 8572/* Build/persist last list URL + location label from filter state as a server-owned root path + query. */ 8573(function (window, document) { 8574 'use strict'; 8575 8576 var root = window.MirelaSearch = window.MirelaSearch || {}; 8577 var api = root.lastListUrl = root.lastListUrl || {}; 8578 var route = root.routeUtils || {}; 8579 var TRANSIENT_PERSISTENCE_FIELDS = { 8580 map_user: true, 8581 map_lat: true, 8582 map_lng: true, 8583 map_zoom: true, 8584 extras: true 8585 }; 8586 8587 function getLocalePrefixForCurrentPath() { 8588 try { 8589 if (route && typeof route.getLocalePrefix === 'function') { 8590 return String(route.getLocalePrefix(window.location.pathname || '') || ''); 8591 } 8592 } catch (_e) {} 8593 try { 8594 var htmlLang = (document.documentElement.getAttribute('lang') || 'bg').toLowerCase(); 8595 var seg = (htmlLang.split('-')[0] || '').toLowerCase(); 8596 return (!seg || seg === 'bg') ? '' : ('/' + seg); 8597 } catch (_e2) { 8598 return ''; 8599 } 8600 } 8601 8602 function resolveBasePath(opts) { 8603 try { 8604 var o = opts || {}; 8605 try { 8606 var b = (o.basePath == null) ? '' : String(o.basePath); 8607 if (b && b[0] === '/') return b; 8608 } catch (_e) {} 8609 try { 8610 var p = String(window.location.pathname || ''); 8611 return (p && p[0] === '/') ? p : ''; 8612 } catch (_e2) { return ''; } 8613 } catch (_e3) { 8614 return ''; 8615 } 8616 } 8617 8618 function buildCanonicalPersistedState(st) { 8619 var copy = Object.assign({}, st || {}); 8620 try {
8621 Object.keys(TRANSIENT_PERSISTENCE_FIELDS).forEach(function (key) { 8622 delete copy[key]; 8623 }); 8624 } catch (_e0) {} 8625 try { 8626 ['property_type', 'focus', 'construction_type', 'construction_stage', 'land_only'].forEach(function (key) { 8627 if (copy[key] == null) return; 8628 if (!Array.isArray(copy[key])) copy[key] = [copy[key]]; 8629 copy[key] = copy[key].map(function (value) { return String(value == null ? '' : value).trim(); }).filter(Boolean); 8630 }); 8631 } catch (_e) {} 8632 return copy; 8633 } 8634 8635 function buildDealRootPath(st) { 8636 var prefix = getLocalePrefixForCurrentPath(); 8637 var deal = '/prodazhbi'; 8638 var t = String((st && st.type) || '').trim(); 8639 if (t === '2') deal = '/naemi'; 8640 else if (t === '3') deal = '/kupuvachi'; 8641 else if (t === '4') deal = '/naemateli'; 8642 return prefix + deal; 8643 } 8644 8645 function appendStateValue(params, key, value) { 8646 if (value == null) return; 8647 if (Array.isArray(value)) { 8648 value.forEach(function (item) { 8649 var text = String(item == null ? '' : item).trim(); 8650 if (!text || text === '0') return; 8651 params.append(/\[\]$/.test(key) ? key : (key + '[]'), text); 8652 }); 8653 return; 8654 } 8655 var textValue = String(value).trim(); 8656 if (!textValue || textValue === '0') return; 8657 params.append(key, textValue); 8658 } 8659 8660 function buildSyntheticLastListUrl(st, basePathOverride) { 8661 try { 8662 if (!st) return ''; 8663 var pathOnly = buildDealRootPath(st); 8664 try { 8665 var explicitBase = String(basePathOverride || '').trim(); 8666 if (explicitBase && explicitBase[0] === '/') { 8667 pathOnly = explicitBase.split('?')[0] || pathOnly; 8668 } 8669 } catch (_eBase) {} 8670 8671 var persisted = buildCanonicalPersistedState(st); 8672 try { 8673 delete persisted.type; 8674 } catch (_eType) {} 8675 8676 var params = new URLSearchParams(); 8677 Object.keys(persisted).forEach(function (key) { 8678 if (TRANSIENT_PERSISTENCE_FIELDS[key]) return; 8679 appendStateValue(params, key, persisted[key]); 8680 }); 8681 8682 var query = params.toString(); 8683 return pathOnly + (query ? ('?' + query) : ''); 8684 } catch (_e7) { return ''; } 8685 } 8686 8687 function normalizeCurrentListRelFromWindow(basePathOverride) { 8688 try { 8689 var explicitBase = String(basePathOverride || '').trim(); 8690 var current = String(window.location.pathname || '') + String(window.location.search || ''); 8691 var rel = current && current[0] === '/' ? current : ''; 8692 if (!rel && explicitBase && explicitBase[0] === '/') { 8693 rel = explicitBase; 8694 } 8695 if (!(rel && rel[0] === '/')) return ''; 8696 var svc = root.lastListUrlService; 8697 if (svc && typeof svc.normalizePersistentListRel === 'function') { 8698 rel = svc.normalizePersistentListRel(rel); 8699 } 8700 return rel; 8701 } catch (_eNorm) { 8702 return ''; 8703 } 8704 } 8705 8706 function persistLastListUrlAndLabel(st, opts) { 8707 try { 8708 var o = opts || {}; 8709 var basePath = (o.basePath == null) ? '' : String(o.basePath); 8710 var rel = normalizeCurrentListRelFromWindow(basePath); 8711 if (!rel) { 8712 rel = buildSyntheticLastListUrl(st, basePath); 8713 } 8714 if (!(rel && rel[0] === '/')) return ''; 8715 8716 var svc = root.lastListUrlService; 8717 if (svc && typeof svc.writeLastListContext === 'function') { 8718 var sLabel = (st && st.s != null) ? String(st.s).trim() : ''; 8719 svc.writeLastListContext({ 8720 url: rel, 8721 locationLabel: sLabel, 8722 maxAge: o.maxAge 8723 }); 8724 return rel; 8725 } 8726 return ''; 8727 } catch (_e3) { 8728 return ''; 8729 } 8730 } 8731 8732 api.resolveBasePath = resolveBasePath; 8733 api.buildSyntheticLastListUrl = buildSyntheticLastListUrl; 8734 api.normalizeCurrentListRelFromWindow = normalizeCurrentListRelFromWindow; 8735 api.persistLastListUrlAndLabel = persistLastListUrlAndLabel; 8736})(window, document); 8737 8738 8739// ---- END: js/components/search-last-list-url.js ----
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.