PageSourceSearch

https://www.letsroam.com/assets/js/common/source/search_modern.js?v=5

js letsroam.com collected 2026-09-24 09:24:37 UTC 42,997 bytes, 1,118 lines download raw bytes

1(function() {
2  if (window.SearchModern) {
3    return;
4  }
5
6  var searchModernConfig = window.LR_SEARCH_MODERN_CONFIG || {};
7  var disableLetsRoamSavedSearchPrefill = !!searchModernConfig.disableLetsRoamSavedSearchPrefill;
8  var letsRoamSavedSearchPrefillAllowlist = [
9    'scavenger-hunt',
10    'outdoor-activities',
11    'bar-crawl',
12    'art-walks',
13    'audio',
14    'ghost'
15  ];
16
17  // The classic LR header supplies pageLanguage, not mhCurrentLanguage.
18  var currentLanguage = window.mhCurrentLanguage || window.pageLanguage || 'en';
19  var pageContext = window.mhPageContext || {
20    isBachelorette: window.location.href.includes('bachelorette') || window.location.href.includes('/bach'),
21    isBirthday: window.location.href.includes('birthday') || window.location.href.includes('/birth'),
22    isCorporate: window.location.href.includes('team_building') || window.location.href.includes('team-building') || (typeof IS_CORPORATE !== 'undefined' && IS_CORPORATE)
23  };
24  var smartCitySearchEnabled = searchModernConfig.websiteDomain === 'sch'
25    || (searchModernConfig.websiteDomain === 'letsroam' && !pageContext.isCorporate && !searchModernConfig.isCorporate);
26  var smartCitySearchParam = searchModernConfig.websiteDomain === 'letsroam' && smartCitySearchEnabled ? '&smart_city_search=1' : '';
27
28  function translate(text) {
29    return (typeof window.translateText === 'function') ? window.translateText(text) : text;
30  }
31
32  function shouldRestoreLetsRoamSavedSearchPrefill() {
33    if (!disableLetsRoamSavedSearchPrefill) {
34      return true;
35    }
36
37    var currentPath = (window.location.pathname || '').toLowerCase();
38    for (var i = 0; i < letsRoamSavedSearchPrefillAllowlist.length; i++) {
39      if (currentPath.indexOf(letsRoamSavedSearchPrefillAllowlist[i]) !== -1) {
40        return true;
41      }
42    }
43
44    return false;
45  }
46
47  function shouldRestoreSavedSearchForContainer(container) {
48    if (container && container.getAttribute('data-search-modern-disable-saved-prefill') === '1') {
49      return false;
50    }
51
52    return shouldRestoreLetsRoamSavedSearchPrefill();
53  }
54
55  function isCityOnlyDropdownMode() {
56    return !!window.LR_SEARCH_CITY_ONLY_DROPDOWN;
57  }
58
59  function escapeHtml(str) {
60    if (!str) return '';
61    return String(str)
62      .replace(/&/g, '&amp;')
63      .replace(/</g, '&lt;')
64      .replace(/>/g, '&gt;')
65      .replace(/"/g, '&quot;');
66  }
67
68  function highlightMatch(text, query) {
69    if (!text || !query) return escapeHtml(text);
70    var escaped = escapeHtml(text);
71    var words = query.toLowerCase().split(/\s+/).filter(function(w) { return w.length > 1; });
72    for (var wordIndex = 0; wordIndex < words.length; wordIndex++) {
73      var word = words[wordIndex];
74      var regex = new RegExp('(' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
75      escaped = escaped.replace(regex, '<strong>$1</strong>');
76    }
77    return escaped;
78  }
79
80  function getCityDisplayName(city) {
81    var showState = city.state && (
82      city.country === 'United States'
83      || city.country === 'Canada'
84      || city.country === 'Australia'
85    );
86    return showState ? city.city + ', ' + city.state : (city.unique_city_name || city.city);
87  }
88
89  function localizeUrl(url) {
90    if (!url) {
91      return url;
92    }
93
94    function appendLocalTestingDomain(targetUrl) {
95      var hostname = window.location.hostname;
96      var shouldPreserveTestingParams = hostname.indexOf('.local.com') !== -1
97        || hostname.indexOf('.test-site.') !== -1
98        || hostname.indexOf('test-site.') === 0
99        || hostname.indexOf('.preview.letsroam.com') !== -1;
100
101      if (!targetUrl || !shouldPreserveTestingParams) {
102        return targetUrl;
103      }
104
105      try {
106        var currentUrl = new URL(window.location.href, window.location.origin);
107        var preservedParams = [
108          'autoTest',
109          'ignore_tracking',
110          'testing_website_domain',
111          'utm_source',
112          'utm_medium',
113          'utm_campaign'
114        ];
115        var hasPreservedParam = preservedParams.some(function (paramName) {
116          return currentUrl.searchParams.has(paramName);
117        });
118
119        if (!hasPreservedParam) {
120          return targetUrl;
121        }
122
123        var resolvedUrl = new URL(targetUrl, window.location.origin);
124        preservedParams.forEach(function (paramName) {
125          if (!resolvedUrl.searchParams.has(paramName) && currentUrl.searchParams.has(paramName)) {
126            resolvedUrl.searchParams.set(paramName, currentUrl.searchParams.get(paramName));
127          }
128        });
129
130        if (/^https?:\/\//i.test(targetUrl)) {
131          return resolvedUrl.toString();
132        }
133
134        return resolvedUrl.pathname + resolvedUrl.search + resolvedUrl.hash;
135      } catch (error) {
136        return targetUrl;
137      }
138    }
139
140    if (typeof window.base_url === 'function') {
141      var baseUrlResult = window.base_url(url);
142      if (baseUrlResult && baseUrlResult !== url) {
143        return appendLocalTestingDomain(baseUrlResult);
144      }
145    }
146
147    if (currentLanguage === 'en' || /^(mailto:|tel:|javascript:|#)/i.test(url)) {
148      return appendLocalTestingDomain(url);
149    }
150
151    if (url === '/' + currentLanguage || url.indexOf('/' + currentLanguage + '/') === 0) {
152      return appendLocalTestingDomain(url);
153    }
154
155    if (/^https?:\/\//i.test(url)) {
156      try {
157        var parsedUrl = new URL(url, window.location.origin);
158        if (parsedUrl.pathname !== '/' + currentLanguage && parsedUrl.pathname.indexOf('/' + currentLanguage + '/') !== 0) {
159          parsedUrl.pathname = '/' + currentLanguage + parsedUrl.pathname;
160        }
161        return appendLocalTestingDomain(parsedUrl.toString());
162      } catch (error) {
163        return appendLocalTestingDomain(url);
164      }
165    }
166
167    if (url.charAt(0) === '/') {
168      return appendLocalTestingDomain('/' + currentLanguage + url);
169    }
170
171    return appendLocalTestingDomain(url);
172  }
173
174  function getSearchUrl(city) {
175    if (pageContext.huntOccasion && window.citySearchUtils) {
176      return localizeUrl(window.citySearchUtils.getOccasionSearchUrl(city));
177    }
178    var path = window.location.pathname || '';
179    var host = window.location.hostname || '';
180    var forceThingsToDoRoutes = !!window.LR_USE_THINGS_TO_DO_ROUTES;
181    var isLetsRoamHost = host.indexOf('letsroam.') !== -1 || host.indexOf('letsroam.local') !== -1;
182    var isThingsToDoContext = path.indexOf('/things-to-do') === 0 || path.indexOf('/activities/find_city') === 0 || path.indexOf('/tours') === 0;
183    var useThingsToDoRoutes = forceThingsToDoRoutes || isLetsRoamHost || isThingsToDoContext;
184    var huntCitySlug = (city && (city.hunt_city_slug || city.slug || city.hunt_region_slug)) ? (city.hunt_city_slug || city.slug || city.hunt_region_slug) : '';
185
186    if (pageContext.isBachelorette || pageContext.isBirthday || pageContext.isCorporate) {
187      var url = (useThingsToDoRoutes ? '/things-to-do/search?lat=' : '/outdoor-activities/search?latitude=') + encodeURIComponent(city.lat) +
188        (useThingsToDoRoutes ? '&long=' : '&longitude=') + encodeURIComponent(city.long) +
189        '&keyword=' + encodeURIComponent(city.city || city.unique_city_name || '');
190      if (pageContext.isBachelorette) url += '&is_bach=true';
191      else if (pageContext.isBirthday) url += '&is_birthday=true';
192      else if (pageContext.isCorporate) url += '&is_corporate=true';
193      return localizeUrl(url);
194    }
195
196    if (useThingsToDoRoutes) {
197      if (city.source === 'geocode' || !city.id || !huntCitySlug) {
198        return localizeUrl('/things-to-do/search?lat=' + encodeURIComponent(city.lat) + '&long=' + encodeURIComponent(city.long));
199      }
200      return localizeUrl('/things-to-do/' + huntCitySlug);
201    }
202
203    if (city.source === 'geocode' || !city.id) {
204      return localizeUrl('/outdoor-activities/search?latitude=' + encodeURIComponent(city.lat) + '&longitude=' + encodeURIComponent(city.long));
205    }
206    return localizeUrl('/outdoor-activities/' + huntCitySlug);
207  }
208
209  function fetchJsonWithStandardClient(url, requestOptions) {
210    if (typeof window.fetchWithParams === 'function') {
211      return window.fetchWithParams(url, {}, {}, 'GET').then(function(data) {
212        if (data && data.status === false) {
213          throw new Error(data.error || 'api_error');
214        }
215        return data || {};
216      });
217    }
218
219    return fetch(url, requestOptions || {})
220      .then(function(res) {
221        if (!res.ok) throw new Error('api_error');
222        return res.json();
223      });
224  }
225
226  function showSearchButtonLoading(searchBtn) {
227    if (!searchBtn) {
228      return;
229    }
230    searchBtn.classList.add('mh-search-btn--loading');
231    setTimeout(function() {
232      searchBtn.classList.remove('mh-search-btn--loading');
233    }, 5000);
234  }
235
236  function hideLegacyContinueCta(element) {
237    if (!element) {
238      return;
239    }
240    element.classList.add('lr-search-modern-hidden-continue');
241    element.setAttribute('aria-hidden', 'true');
242    element.setAttribute('hidden', 'hidden');
243    element.style.setProperty('display', 'none', 'important');
244    element.style.setProperty('visibility', 'hidden', 'important');
245    element.style.setProperty('pointer-events', 'none', 'important');
246  }
247
248  function updateClearButton(state) {
249    if (!state || !state.clearBtn || !state.input) {
250      return;
251    }
252    if (state.input.value.length > 0) {
253      state.clearBtn.classList.add('visible');
254    } else {
255      state.clearBtn.classList.remove('visible');
256    }
257  }
258
259  function showLoading(state) {
260    state.isLoading = true;
261    if (!state.dropdown) return;
262    var skeletonHtml = '';
263    for (var i = 0; i < 4; i++) {
264      skeletonHtml += '<div class="mh-skeleton-item">' +
265        '<div class="mh-skeleton-icon"></div>' +
266        '<div class="mh-skeleton-text">' +
267          '<div class="mh-skeleton-line"></div>' +
268          '<div class="mh-skeleton-line short"></div>' +
269        '</div>' +
270        '<div class="mh-skeleton-line count"></div>' +
271      '</div>';
272    }
273    state.dropdown.innerHTML = skeletonHtml;
274    openDropdown(state);
275  }
276
277  function showEngagementState(state, iconCount) {
278    if (!state || !state.dropdown) {
279      return;
280    }
281    var html = '';
282    var iconSvg = '<svg class="mh-dropdown-item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
283      '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>' +
284      '<circle cx="12" cy="10" r="3"></circle>' +
285    '</svg>';
286    for (var i = 0; i < iconCount; i++) {
287      html += '<div class="mh-skeleton-item">' +
288        iconSvg +
289        '<div class="mh-skeleton-text">' +
290          '<div class="mh-skeleton-line"></div>' +
291          '<div class="mh-skeleton-line short"></div>' +
292        '</div>' +
293        '<div class="mh-skeleton-line count"></div>' +
294      '</div>';
295    }
296    for (var j = iconCount; j < 4; j++) {
297      html += '<div class="mh-skeleton-item">' +
298        '<div class="mh-skeleton-icon"></div>' +
299        '<div class="mh-skeleton-text">' +
300          '<div class="mh-skeleton-line"></div>' +
301          '<div class="mh-skeleton-line short"></div>' +
302        '</div>' +
303      '</div>';
304    }
305    state.dropdown.innerHTML = html;
306  }
307
308  function openDropdown(state) {
309    if (!state.dropdown) return;
310    state.dropdown.classList.add('open');
311    state.isOpen = true;
312    if (window.citySearchUtils && typeof window.citySearchUtils.updateScrollFade === 'function') {
313      setTimeout(function() { window.citySearchUtils.updateScrollFade(state.dropdown); }, 50);
314    }
315  }
316
317  function closeDropdown(state) {
318    if (!state.dropdown) return;
319    state.dropdown.classList.remove('open');
320    state.isOpen = false;
321    state.isLoading = false;
322    state.highlightedIndex = -1;
323    if (window.citySearchUtils && typeof window.citySearchUtils.clearScrollFade === 'function') {
324      window.citySearchUtils.clearScrollFade(state.dropdown);
325    }
326  }
327
328  function applyTopHuntsData(state, city, index, data) {
329    // The query can change before this lazy request finishes.
330    if (state.cities[index] !== city) return;
331    var hunts = data.hunts || [];
332    var nearbyCount = parseInt(data.nearby_hunts) || 0;
333    var activityCount = parseInt(city.activities_in_city) || 0;
334
335    var container = state.dropdown.querySelector('.mh-dropdown-hunts-container[data-city-index="' + index + '"]');
336    if (container) container.innerHTML = '';
337
338    state.cities[index].top_hunts = hunts;
339    state.cities[index].nearby_hunts = nearbyCount;
340
341    var countEl = state.dropdown.querySelector('.mh-dropdown-item[data-index="' + index + '"] .mh-dropdown-item-count');
342    if (countEl) {
343      var badgeEl = countEl.querySelector('.mh-count-badge');
344      var nearbyEl = countEl.querySelector('.mh-nearby-text');
345      if (activityCount === 0 && nearbyCount > 0) {
346        if (!badgeEl) {
347          countEl.insertAdjacentHTML('afterbegin', '<span class="mh-count-badge">' + nearbyCount + ' nearby</span>');
348        } else {
349          badgeEl.textContent = nearbyCount + ' ' + translate('nearby');
350        }
351        if (nearbyEl) nearbyEl.textContent = '';
352      } else if (activityCount > 0 && nearbyCount > activityCount && nearbyEl) {
353        nearbyEl.textContent = '(' + nearbyCount + ' ' + translate('nearby') + ')';
354      }
355    }
356
357    if (!container || hunts.length === 0) return;
358
359    var searchQuery = state.input.value || '';
360    container.innerHTML = hunts.map(function(hunt, huntIndex) {
361      var isInCity = parseInt(hunt.is_in_city) === 1;
362      var showBadge = huntIndex === 0 && isInCity && activityCount >= 1;
363      var badge = showBadge ? '<span class="mh-top-hunt-badge">' + translate('Top Hunt') + '</span>' : '';
364      var distance = parseFloat(hunt.distance_miles) || 0;
365      var distanceText = (!isInCity && distance > 0) ? '<span class="mh-hunt-distance">
365' + distance.toFixed(1) + ' mi</span>' : '';
366
367      return '<div class="mh-dropdown-hunt" data-hunt-url="' + escapeHtml(hunt.hunt_activity_url || '') + '" data-city-index="' + index + '">' +
368        '<span class="mh-dropdown-hunt-name">' + highlightMatch(hunt.name || '', searchQuery) + '</span>' +
369        '<span class="mh-hunt-meta">' + distanceText + badge + '</span>' +
370      '</div>';
371    }).join('');
372
373    var huntItems = container.querySelectorAll('.mh-dropdown-hunt');
374    for (var huntIndex = 0; huntIndex < huntItems.length; huntIndex++) {
375      (function(item) {
376        item.addEventListener('touchend', function(e) {
377          handleHuntClick(state, item, e);
378        });
379        item.addEventListener('mousedown', function(e) {
380          handleHuntClick(state, item, e);
381        });
382      })(huntItems[huntIndex]);
383    }
384  }
385
386  function lazyLoadTopHunts(state) {
387    if (pageContext.huntOccasion || pageContext.isBachelorette || pageContext.isBirthday || pageContext.isCorporate) {
388      var firstCities = state.cities.slice(0, 5);
389      for (var idx = 0; idx < firstCities.length; idx++) {
390        var city = firstCities[idx];
391        var container = state.dropdown.querySelector('.mh-dropdown-hunts-container[data-city-index="' + idx + '"]');
392        if (container) container.innerHTML = '';
393      }
394      return;
395    }
396
397    var topCities = state.cities.slice(0, 5);
398    for (var index = 0; index < topCities.length; index++) {
399      (function(city, cityIndex) {
400        if ((!city.lat || !city.long) && !city.id) {
401          return;
402        }
403
404        if (state.cities[cityIndex] && state.cities[cityIndex].top_hunts && state.cities[cityIndex].top_hunts.length) {
405          applyTopHuntsData(state, city, cityIndex, { hunts: state.cities[cityIndex].top_hunts, nearby_hunts: state.cities[cityIndex].nearby_hunts || 0 });
406          return;
407        }
408
409        var latRounded = Math.round(parseFloat(city.lat) * 100) / 100;
410        var lngRounded = Math.round(parseFloat(city.long) * 100) / 100;
411        var cacheKey = 'mh_top_hunts_v10_' + currentLanguage + '_' + (city.id || '') + '_' + latRounded + '_' + lngRounded + '_3_30';
412        try {
413          var cached = localStorage.getItem(cacheKey);
414          if (cached) {
415            var parsed = JSON.parse(cached);
416            if (parsed && parsed.expires > Date.now() && parsed.data) {
417              applyTopHuntsData(state, city, cityIndex, parsed.data);
418              return;
419            }
420          }
421        } catch (e) {}
422
423        var topHuntsUrl = city.id
424          ? ('/api/city/top_hunts?city_id=' + encodeURIComponent(city.id) + '&limit=3&radius=30&lang=' + currentLanguage)
425          : ('/api/city/top_hunts?lat=' + city.lat + '&lng=' + city.long + '&limit=3&radius=30&lang=' + currentLanguage);
426
427        fetchJsonWithStandardClient(topHuntsUrl)
428          .then(function(data) {
429            applyTopHuntsData(state, city, cityIndex, data || {});
430            try {
431              localStorage.setItem(cacheKey, JSON.stringify({
432                data: data || {},
433                expires: Date.now() + (24 * 60 * 60 * 1000)
434              }));
435            } catch (e) {}
436          })
437          .catch(function() {
438            applyTopHuntsData(state, city, cityIndex, {});
439          });
440      })(topCities[index], index);
441    }
442  }
443
444  function setSearchResults(state, data) {
445    state.cities = data.cities || [];
446    state.highlightedIndex = -1;
447    state.recoveryType = data.recovery_type || (state.cities[0] && state.cities[0].recovery_type) || '';
448    var recoveryLabels = {
449      nearby_hunts: 'Nearby hunts',
450      closest_supported: 'Closest supported cities',
451      popular_alternatives: 'Popular alternatives'
452    };
453    state.recoveryLabel = data.recovery_label || recoveryLabels[state.recoveryType] || '';
454  }
455
456  function cacheSearchResults(state, cacheKey, suggestions) {
457    try {
458      localStorage.setItem(cacheKey, JSON.stringify({
459        cities: state.cities,
460        recovery_type: state.recoveryType,
461        recovery_label: state.recoveryLabel,
462        did_you_mean: suggestions,
463        expires: Date.now() + (60 * 60 * 1000)
464      }));
465    } catch (e) {}
466  }
467
468  function renderSpellingSuggestions(state, suggestions) {
469    if (!suggestions || !suggestions.length || !window.citySearchUtils || typeof window.citySearchUtils.renderDidYouMean !== 'function') {
470      return false;
471    }
472    window.citySearchUtils.renderDidYouMean(suggestions, state.dropdown, function() { openDropdown(state); }, state.input, function(newQuery) {
473      fetchCities(state, newQuery, false);
474    });
475    return true;
476  }
477
478  function renderDropdown(state) {
479    if (!state.cities.length) {
480      state.dropdown.innerHTML = '<div class="mh-dropdown-empty">No cities found</div>';
481      openDropdown(state);
482      return;
483    }
484
485    var searchQuery = state.input.value || '';
486    var recoveryHeading = state.recoveryLabel
487      ? '<div class="mh-recovery-heading h-padding__v--10 h-padding__h--15 h-color--gray3 h-size--13 h-weight--700">'
488        + escapeHtml(translate(state.recoveryLabel)) + '</div>'
489      : '';
490    state.dropdown.innerHTML = recoveryHeading + state.cities.map(function(city, index) {
491      var displayName = getCityDisplayName(city);
492      var activityCount = parseInt(city.activities_in_city) || 0;
493      var countBadge = activityCount > 0 ? '<span class="mh-count-badge">' + activityCount + ' ' + translate(activityCount !== 1 ? 'hunts' : 'hunt') + '</span>' : '';
494
495      var cityRow = '<div class="mh-dropdown-item' + (index === state.highlightedIndex ? ' highlighted' : '') + '" data-index="' + index + '">' +
496        '<svg class="mh-dropdown-item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
497          '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>' +
498          '<circle cx="12" cy="10" r="3"></circle>' +
499        '</svg>' +
500        '<div class="mh-dropdown-item-text">' +
501          '<div class="mh-dropdown-item-city">' + highlightMatch(displayName, searchQuery) + '</div>' +
502          '<div class="mh-dropdown-item-meta">' + highlightMatch(city.country || '', searchQuery) + '</div>' +
503        '</div>' +
504        '<div class="mh-dropdown-item-right">' +
505          '<span class="mh-dropdown-item-count">' + countBadge + '<span class="mh-nearby-text"></span></span>' +
506          '<svg class="mh-dropdown-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>' +
507        '</div>' +
508      '</div>';
509
510      if (isCityOnlyDropdownMode()) {
511        return cityRow;
512      }
513
514      var huntsSkeleton = index < 5
515        ? '<div class="mh-skeleton-hunt"><div class="mh-skeleton-line" style="width:70%"></div></div><div class="mh-skeleton-hunt"><div class="mh-skeleton-line" style="width:55%"></div></div>'
516        : '';
517      return cityRow + '<div class="mh-dropdown-hunts-container" data-city-index="' + index + '">' + huntsSkeleton + '</div>';
518    }).join('');
519
520    var dropdownItems = state.dropdown.querySelectorAll('.mh-dropdown-item');
521    for (var dropdownIndex = 0; dropdownIndex < dropdownItems.length; dropdownIndex++) {
522      (function(item) {
523        var handleSelect = function(e) {
524          if (e.type === 'touchend' && state.scrolledDuringTouch) {
525            return;
526          }
527          e.preventDefault();
528          var index = parseInt(item.dataset.index);
529          if (!state.cities[index]) return;
530          item.style.background = '#e8e8e8';
531          closeDropdown(state);
532          selectCity(state, state.cities[index]);
533        };
534        item.addEventListener('touchend', handleSelect);
535        item.addEventListener('mousedown', handleSelect);
536      })(dropdownItems[dropdownIndex]);
537    }
538
539    openDropdown(state);
540    if (!isCityOnlyDropdownMode()) {
541      lazyLoadTopHunts(state);
542    }
543  }
544
545  function handleHuntClick(state, item, event) {
546    if (!state || !item || !event) {
547      return;
548    }
549    if (event.type === 'touchend' && state.scrolledDuringTouch) {
550      return;
551    }
552    event.preventDefault();
553    event.stopPropagation();
554    var huntUrl = item.dataset.huntUrl;
555    var cityIndex = parseInt(item.dataset.cityIndex, 10);
556    if (!huntUrl) {
557      return;
558    }
559
560    item.style.background = '#e0e0e0';
561    closeDropdown(state);
562    showSearchButtonLoading(state.searchBtn);
563
564    if (!isNaN(cityIndex) && state.cities[cityIndex]) {
565      var city = state.cities[cityIndex];
566      var displayName = getCityDisplayName(city);
567      state.input.value = displayName;
568      updateClearButton(state);
569      try {
570        localStorage.setItem('sch_header_search_label', displayName);
571      } catch (e) {}
572    }
573
574    window.location.href = localizeUrl(huntUrl);
575  }
576
577  function selectCity(state, city) {
578    var displayName = getCityDisplayName(city);
579    state.input.value = displayName;
580    updateClearButton(state);
581    try { localStorage.setItem('sch_header_search_label', displayName); } catch (e) {}
582    closeDropdown(state);
583    showSearchButtonLoading(state.searchBtn);
584    window.location.href = getSearchUrl(city);
585  }
586
587  function fetchFromGeocode(state, query, cacheKey, didYouMeanSuggestions, selectFirstOnLoad) {
588    showLoading(state);
589    fetchJsonWithStandardClient('/api/city/search_geocode?q=' + encodeURIComponent(query) + '&limit=5&lang=' + currentLanguage + smartCitySearchParam)
590      .then(function(data) {
591        if (query !== state.input.value.trim()) return;
592        state.isLoading = false;
593        setSearchResults(state, data);
594        if (state.recoveryType === 'popular_alternatives' && renderSpellingSuggestions(state, didYouMeanSuggestions)) {
595          setSearchResults(state, {});
596          cacheSearchResults(state, cacheKey, didYouMeanSuggestions);
597          return;
598        }
599        if (state.cities.length > 0) {
600          renderDropdown(state);
601          if (selectFirstOnLoad && !state.recoveryType) selectCity(state, state.cities[0]);
602          cacheSearchResults(state, cacheKey);
603          return;
604        }
605        if (renderSpellingSuggestions(state, didYouMeanSuggestions)) return;
606        state.dropdown.innerHTML = '<div class="mh-dropdown-empty">No cities found. Please check your spelling or try a different search.</div>';
607        openDropdown(state);
608      })
609      .catch(function() {
610        if (query !== state.input.value.trim()) return;
611        state.isLoading = false;
612        setSearchResults(state, {});
613        if (renderSpellingSuggestions(state, didYouMeanSuggestions)) return;
614        state.dropdown.innerHTML = '<div class="mh-dropdown-empty">No cities found. Please check your spelling or try a different search.</div>';
615        openDropdown(state);
616      });
617  }
618
619  function fetchCities(state, query, selectFirstOnLoad) {
620    setSearchResults(state, {});
621    var cacheNamespace = 'city_search_v7_1';
622    if (smartCitySearchEnabled) {
623      // Smart results can depend on the visitor's country. Keep this component's
624      // cache separate from older results and the navbar's recovery metadata.
625      cacheNamespace = 'city_search_modern_v2_' + searchModernConfig.websiteDomain + '_' + (searchModernConfig.visitorCountryCode || '');
626    }
627    var cacheKey = cacheNamespace + '_' + currentLanguage + '_' + query.toLowerCase();
628    try {
629      var cached = localStorage.getItem(cacheKey);
630      if (cached) {
631        var parsed = JSON.parse(cached);
632        if (parsed.expires > Date.now()) {
633          state.isLoading = false;
634          setSearchResults(state, parsed);
635          if (state.cities.length === 0 && renderSpellingSuggestions(state, parsed.did_you_mean)) return;
636          renderDropdown(state);
637          if (selectFirstOnLoad && state.cities.length > 0 && !state.recoveryType) selectCity(state, state.cities[0]);
638          return;
639        }
640      }
641    } catch (e) {}
642
643    var requestToken = String(Date.now()) + '-' + Math.random();
644    state.currentRequest = requestToken;
645    var dataLoaded = false;
646    var engagementTimers = [];
647
648    engagementTimers.push(setTimeout(function() {
649      if (!dataLoaded && state.isLoading) {
650        showEngagementState(state, 1);
651      }
652    }, 1300));
653
654    engagementTimers.push(setTimeout(function() {
655      if (!dataLoaded && state.isLoading) {
656        showEngagementState(state, 2);
657      }
658    }, 2000));
659
660    engagementTimers.push(setTimeout(function() {
661      if (!dataLoaded && state.isLoading) {
662        showEngagementState(state, 3);
663      }
664    }, 3000));
665
666    fetchJsonWithStandardClient('/api/city/search_autocomplete?q=' + encodeURIComponent(query) + '&limit=8&lang=' + currentLanguage + smartCitySearchParam)
667      .then(function(data) {
668        dataLoaded = true;
669        for (var timerIndex = 0; timerIndex < engagementTimers.length; timerIndex++) {
670          clearTimeout(engagementTimers[timerIndex]);
671        }
672        if (state.currentRequest !== requestToken) return;
673        if (query !== state.input.value.trim()) return;
674        state.isLoading = false;
675        setSearchResults(state, data);
676        if (state.cities.length === 0) {
677          fetchFromGeocode(state, query, cacheKey, data.did_you_mean, selectFirstOnLoad);
678          return;
679        }
680
681        renderDropdown(state);
682        if (selectFirstOnLoad && !state.recoveryType) selectCity(state, state.cities[0]);
683        cacheSearchResults(state, cacheKey);
684      })
685      .catch(function(err) {
686        dataLoaded = true;
687        for (var timerIndex = 0; timerIndex < engagementTimers.length; timerIndex++) {
688          clearTimeout(engagementTimers[timerIndex]);
689        }
690        if (state.currentRequest !== requestToken) return;
691        state.isLoading = false;
692        setSearchResults(state, {});
693        state.dropdown.innerHTML = '<div class="mh-dropdown-empty">Search unavailable</div>';
694        openDropdown(state);
695      });
696  }
697
698  function mount(config) {
699    if (!config || !config.input || !config.dropdown) return null;
700    var container = config.container || (config.input.closest ? config.input.closest('.mh-search-container') : null);
701    var state = {
702      input: config.input,
703      dropdown: config.dropdown,
704      searchBtn: config.searchBtn || null,
705      clearBtn: config.clearBtn || null,
706      container: container,
707      cities: [],
708      highlightedIndex: -1,
709      recoveryType: '',
710      recoveryLabel: '',
711      isOpen: false,
712      isLoading: false,
713      debounceTimer: null,
714      currentRequest: null,
715      scrolledDuringTouch: false
716    };
717
718    state.input.addEventListener('input', function() {
719      updateClearButton(state);
720    });
721
722    state.input.addEventListener('input', function() {
723      var query = state.input.value.trim();
724      setSearchResults(state, {});
725      if (query.length < 2) {
726        state.isLoading = false;
727        closeDropdown(state);
728        return;
729      }
730      showLoading(state);
731      clearTimeout(state.debounceTimer);
732      state.debounceTimer = setTimeout(function() {
733        fetchCities(state, query, false);
734      }, 300);
735    });
736
737    state.input.addEventListener('focus', function() {
738      var query = state.input.value.trim();
739      if (state.cities.length > 0 && query.length >= 2) {
740        openDropdown(state);
741        return;
742      }
743      if (query.length >= 2) {
744        showLoading(state);
745        fetchCities(state, query, false);
746      }
747    });
748
749    state.input.addEventListener('blur', function() {
750      if (window.innerWidth < 1000 && state.cities.length > 0) return;
751      setTimeout(function() { closeDropdown(state); }, 200);
752    });
753
754    state.input.addEventListener('keydown', function(e) {
755      if (!state.isOpen) {
756        if (e.key === 'Enter' && state.input.value.trim().length >= 2) {
757          fetchCities(state, state.input.value.trim(), false);
758        }
759        return;
760      }
761
762      if (e.key === 'ArrowDown') {
763        e.preventDefault();
764        state.highlightedIndex = Math.min(state.highlightedIndex + 1, state.cities.length - 1);
765        renderDropdown(state);
766      } else if (e.key === 'ArrowUp') {
767        e.preventDefault();
768        state.highlightedIndex = Math.max(state.highlightedIndex - 1, 0);
769        renderDropdown(state);
770      } else if (e.key === 'Enter') {
771        e.preventDefault();
772        if (state.highlightedIndex >= 0 && state.cities[state.highlightedIndex]) {
773          selectCity(state, state.cities[state.highlightedIndex]);
774        } else if (state.cities.length > 0 && !state.recoveryType) {
775          selectCity(state, state.cities[0]);
776        }
777      } else if (e.key === 'Escape') {
778        closeDropdown(state);
779      }
780    });
781
782    state.dropdown.addEventListener('touchstart', function() {
783      state.scrolledDuringTouch = false;
784    }, { passive: true });
785
786    state.dropdown.addEventListener('touchmove', function() {
787      state.scrolledDuringTouch = true;
788      if (document.activeElement === state.input) {
789        state.input.blur();
790      }
791    }, { passive: true });
792
793    state.dropdown.addEventListener('scroll', function() {
794      state.scrolledDuringTouch = true;
795      if (window.citySearchUtils && typeof window.citySearchUtils.updateScrollFade === 'function') {
796        window.citySearchUtils.updateScrollFade(state.dropdown);
797      }
798    });
799
800    document.addEventListener('click', function(event) {
801      if (!state.isOpen) {
802        return;
803      }
804      if (window.innerWidth >= 1000) {
805        return;
806      }
807      var searchContainer = state.container;
808      if (searchContainer && !searchContainer.contains(event.target)) {
809        closeDropdown(state);
810      }
811    });
812
813    if (state.clearBtn) {
814      state.clearBtn.addEventListener('click', function(e) {
815        e.preventDefault();
816        state.input.value = '';
817        state.isLoading = false;
818        setSearchResults(state, {});
819        updateClearButton(state);
820        closeDropdown(state);
821        state.input.focus();
822        try {
823          localStorage.removeItem('sch_header_search_label');
824          localStorage.removeItem('sch_header_search_city_id');
825        } catch (err) {}
826      });
827    }
828
829    if (state.searchBtn) {
830      state.searchBtn.addEventListener('mousedown', function(e) {
831        if (state.isOpen) {
832          e.preventDefault();
833          e.stopPropagation();
834        }
835      });
836
837      state.searchBtn.addEventListener('click', function(e) {
838        e.preventDefault();
839        e.stopPropagation();
840        if (state.cities.length > 0) {
841          if (state.recoveryType && state.highlightedIndex < 0) {
842            openDropdown(state);
843            return;
844          }
845          var idx = state.highlightedIndex >= 0 ? state.highlightedIndex : 0;
846          selectCity(state, state.cities[idx]);
847          return;
848        }
849        if (state.isLoading) {
850          return;
851        }
852        var query = state.input.value.trim();
853        if (query.length >= 2) {
854          fetchCities(state, query, true);
855        } else {
856          state.input.focus();
857        }
858      });
859    }
860
861    try {
862      var savedSearchLabel = localStorage.getItem('sch_header_search_label');
863      if (savedSearchLabel && shouldRestoreSavedSearchForContainer(state.container)) {
864        state.input.value = savedSearchLabel;
865      }
866    } catch (e) {}
867    updateClearButton(state);
868
869    return state;
870  }
871
872  function mountContainer(container) {
873    if (!container || container.getAttribute('data-search-modern-mounted') === '1') {
874      return null;
875    }
876
877    var input = container.querySelector('[data-search-modern-input], input[type="text"], input[type="search"]');
878    if (!input) {
879      return null;
880    }
881
882    var dropdown = container.querySelector('.mh-dropdown');
883    if (!dropdown) {
884      dropdown = document.createElement('div');
885      dropdown.className = 'mh-dropdown lr-amazon-inline-search__dropdown';
886      container.appendChild(dropdown);
887    }
888
889    var clearBtn = container.querySelector('.mh-clear-btn');
890    if (!clearBtn) {
891      clearBtn = document.createElement('button');
892      clearBtn.type = 'button';
893      clearBtn.className = 'mh-clear-btn';
894      clearBtn.setAttribute('aria-label', 'Clear search');
895      clearBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
896      container.appendChild(clearBtn);
897    }
898
899    var searchBtn = container.querySelector('.mh-search-btn');
900    if (!searchBtn && container.getAttribute('data-search-modern-button') !== 'none') {
901      searchBtn = document.createElement('button');
902      searchBtn.type = 'button';
903      searchBtn.className = 'mh-search-btn';
904      searchBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5"><circle cx="10" cy="10" r="7"></circle><line x1="20" y1="20" x2="15" y2="15"></line></svg>';
905      container.appendChild(searchBtn);
906    }
907
908    container.classList.add('mh-search-container');
909    input.classList.add('mh-city-input');
910    input.setAttribute('autocomplete', 'off');
911    input.setAttribute('spellcheck', 'false');
912
913    container.setAttribute('data-search-modern-mounted', '1');
914    return mount({
915      container: container,
916      input: input,
917      dropdown: dropdown,
918      clearBtn: clearBtn,
919      searchBtn: searchBtn
920    });
921  }
922
923  function enhanceGeAutocomplete(geInput, options) {
924    if (!geInput || geInput.getAttribute('data-search-modern-enhanced') === '1') return null;
925    geInput.setAttribute('data-search-modern-enhanced', '1');
926    options = options || {};
927
928    var geId = geInput.getAttribute('id') || '';
929    var placeholder = geInput.getAttribute('placeholder') || 'Enter a city to explore';
930    var input = null;
931    var dropdown = null;
932    var searchBtn = null;
933    var clearBtn = null;
934    var componentContainer = null;
935
936    if (options.reuseInputSelector) {
937      input = document.querySelector(options.reuseInputSelector);
938      if (!input) return null;
939      componentContainer = input.closest ? input.closest('.mh-search-container') : input.parentNode;
940      if (!componentContainer) {
941        componentContainer = input.parentNode;
942      }
943      if (componentContainer && componentContainer.classList) {
944        componentContainer.classList.add('mh-search-container');
945      }
946      input.classList.add('mh-city-input');
947      input.style.fontFamily = '"Plus Jakarta Sans", Arial, sans-serif';
948      dropdown = document.createElement('div');
949      dropdown.className = 'mh-dropdown lr-amazon-inline-search__dropdown';
950      input.parentNode.appendChild(dropdown);
951      if (options.clearSelector) {
952        clearBtn = document.querySelector(options.clearSelector);
953      }
954      if (options.searchButtonSelector) {
955        searchBtn = document.querySelector(options.searchButtonSelector);
956      }
957    } else {
958      var wrapper = document.createElement('div');
959      wrapper.className = 'lr-amazon-inline-search mh-search-container';
960      componentContainer = wrapper;
961      input = document.createElement('input');
962      input.type = 'text';
963      input.className = 'mh-city-input lr-amazon-inline-search__input';
964      input.placeholder = placeholder;
965      input.autocomplete = 'off';
966      input.spellcheck = false;
967      input.style.fontFamily = '"Plus Jakarta Sans", Arial, sans-serif';
968
969      clearBtn = document.createElement('button');
970      clearBtn.type = 'button';
971      clearBtn.className = 'mh-clear-btn';
972      clearBtn.setAttribute('aria-label', 'Clear search');
973      clearBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
974
975      dropdown = document.createElement('div');
976      dropdown.className = 'mh-dropdown lr-amazon-inline-search__dropdown';
977
978      searchBtn = document.createElement('button');
979      searchBtn.type = 'button';
980      searchBtn.className = 'mh-search-btn';
981      searchBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5"><circle cx="10" cy="10" r="7"></circle><line x1="20" y1="20" x2="15" y2="15"></line></svg>';
982
983      wrapper.appendChild(input);
984      wrapper.appendChild(clearBtn);
985      wrapper.appendChild(dropdown);
986      wrapper.appendChild(searchBtn);
987      geInput.parentNode.insertBefore(wrapper, geInput);
988    }
989
990    geInput.style.display = 'none';
991    geInput.setAttribute('aria-hidden', 'true');
992
993    var mountedState = mount({
994      container: componentContainer,
995      input: input,
996      dropdown: dropdown,
997      clearBtn: clearBtn,
998      searchBtn: searchBtn
999    });
1000
1001    if (options.bindLegacyButtonsByGeId !== false && geId && mountedState) {
1002      var legacyTriggers = document.querySelectorAll('button[onclick*="' + geId + '"], a[onclick*="' + geId + '"], [role="button"][onclick*="' + geId + '"]');
1003      for (var buttonIndex = 0; buttonIndex < legacyTriggers.length; buttonIndex++) {
1004        var trigger = legacyTriggers[buttonIndex];
1005        var triggerLabel = (trigger.textContent || '').toLowerCase();
1006        if (options.hideLegacyContinueButtons && triggerLabel.indexOf('continue') !== -1) {
1007          hideLegacyContinueCta(trigger);
1008          continue;
1009        }
1010        trigger.removeAttribute('onclick');
1011        trigger.addEventListener('click', function(e) {
1012          e.preventDefault();
1013          e.stopPropagation();
1014          if (mountedState.cities.length > 0) {
1015            if (mountedState.recoveryType && mountedState.highlightedIndex < 0) {
1016              openDropdown(mountedState);
1017              return;
1018            }
1019            var cityIndex = mountedState.highlightedIndex >= 0 ? mountedState.highlightedIndex : 0;
1020            selectCity(mountedState, mountedState.cities[cityIndex]);
1021            return;
1022          }
1023          var query = mountedState.input.value.trim();
1024          if (query.length >= 2) {
1025            showLoading(mountedState);
1026            fetchCities(mountedState, query, true);
1027            return;
1028          }
1029          mountedState.input.focus();
1030        });
1031      }
1032
1033      if (options.hideLegacyContinueButtons && geInput.parentNode) {
1034        var localSearchActions = geInput.parentNode.querySelectorAll('button, a, [role="button"]');
1035        for (var actionIndex = 0; actionIndex < localSearchActions.length; actionIndex++) {
1036          var actionEl = localSearchActions[actionIndex];
1037          var actionLabel = (actionEl.textContent || '').toLowerCase();
1038          var isContinueAction = actionLabel.indexOf('continue') !== -1;
1039          var isSearchAction = actionEl.classList && actionEl.classList.contains('lr-btn-algolia-submit');
1040          if (isContinueAction && isSearchAction) {
1041            hideLegacyContinueCta(actionEl);
1042          }
1043        }
1044      }
1045
1046      if (options.hideLegacyContinueButtons) {
1047        var knownContinueSelectors = [
1048          '#main_anything_where_people_g_o',
1049          '#main_anything_where_people_g_o-button',
1050          '#second_anything_where_people_g_o',
1051          '#second_anything_where_people_g_o-button'
1052        ];
1053        for (var knownIndex = 0; knownIndex < knownContinueSelectors.length; knownIndex++) {
1054          var knownBtn = document.querySelector(knownContinueSelectors[knownIndex]);
1055          if (knownBtn) {
1056            hideLegacyContinueCta(knownBtn);
1057          }
1058        }
1059      }
1060    }
1061
1062    return mountedState;
1063  }
1064
1065  function enhanceAllGeAutocomplete(selector, optionsFactory) {
1066    var geInputs = document.querySelectorAll(selector || 'ge-autocomplete');
1067    for (var geIndex = 0; geIndex < geInputs.length; geIndex++) {
1068      var geInput = geInputs[geIndex];
1069      var options = (typeof optionsFactory === 'function') ? optionsFactory(geInput) : {};
1070      if (options === false) {
1071        continue;
1072      }
1073      enhanceGeAutocomplete(geInput, options || {});
1074    }
1075  }
1076
1077  function mountAllContainers(selector) {
1078    var containers = document.querySelectorAll(selector || '.search_modern');
1079    for (var containerIndex = 0; containerIndex < containers.length; containerIndex++) {
1080      mountContainer(containers[containerIndex]);
1081    }
1082  }
1083
1084  function autoInitContainers() {
1085    mountAllContainers('.search_modern');
1086  }
1087
1088  function startAutoMounting() {
1089    autoInitContainers();
1090    if (typeof MutationObserver !== 'undefined' && document.body) {
1091      var searchModernObserver = new MutationObserver(function(mutations) {
1092        for (var i = 0; i < mutations.length; i++) {
1093          if (!mutations[i].addedNodes || !mutations[i].addedNodes.length) {
1094            continue;
1095          }
1096          autoInitContainers();
1097          break;
1098        }
1099      });
1100      searchModernObserver.observe(document.body, { childList: true, subtree: true });
1101    }
1102  }
1103
1104  // Wait for complete template markup before creating any missing controls.
1105  if (document.readyState === 'loading') {
1106    document.addEventListener('DOMContentLoaded', startAutoMounting);
1107  } else {
1108    startAutoMounting();
1109  }
1110
1111  window.SearchModern = {
1112    mount: mount,
1113    mountContainer: mountContainer,
1114    mountAllContainers: mountAllContainers,
1115    enhanceGeAutocomplete: enhanceGeAutocomplete,
1116    enhanceAllGeAutocomplete: enhanceAllGeAutocomplete
1117  };
1118})();

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.