1/** 2 * Listing Pages JavaScript 3 * Handles filter sidebar interactions and view toggles 4 * 5 * @package Yatra 6 */ 7 8(function() { 9 'use strict'; 10 11 // Initialize when DOM is ready 12 if (document.readyState === 'loading') { 13 document.addEventListener('DOMContentLoaded', init); 14 } else { 15 init(); 16 } 17 18 function init() { 19 // Filter sidebar toggles (.yatra-filter-title click â toggle .open 20 // on the section) live in listing-filters.js. Keeping a second 21 // copy here meant TWO toggles fired per click, leaving the 22 // section in its original state â i.e. the arrow appeared dead. 23 initViewToggle(); 24 initPriceRange(); 25 initAdvancedSearch(); 26 initTravelersSelect(); 27 initStretchedCardClicks(); 28 // Search functionality moved to shortcode-specific JS 29 // initHorizontalSearchDropdowns(); 30 // initDurationSlider(); 31 } 32 33 // Whole-card click target for the destination / activity / trip-category 34 // shortcode-as-block "category" cards. Trip listing cards intentionally 35 // opt out â those have multiple visible CTAs (View Details button, image 36 // link, title link, favorite, category/activity chips) and shouldn't 37 // hijack ambient clicks; the user navigates via the explicit controls. 38 // 39 // CSS-only "stretched link" (title link ::after with inset:0) is the 40 // canonical implementation and remains in place for screen-reader and 41 // crawler semantics â but in some layouts an intermediate ancestor with 42 // `position: relative` traps the absolute overlay, leaving the upper 43 // portion of the card non-clickable. This delegated click handler is the 44 // belt-and-braces fallback: it catches any click that didn't already land 45 // on a real interactive element and forwards it to the card's primary 46 // link. Modifier keys (ctrl/cmd/middle-click) open in a new tab to match 47 // the browser's normal `<a>` behavior. 48 function initStretchedCardClicks() { 49 const cardSelector = '.yatra-category-card'; 50 const linkSelector = 'a.yatra-category-card-title-link'; 51 // Click targets that should be allowed to handle their own event. 52 const interactive = 'a, button, input, select, textarea, label, [role="button"], [data-no-card-click]'; 53 54 document.addEventListener('click', function (event) { 55 // Allow native links/buttons to do their thing. 56 if (event.target.closest(interactive)) { 57 return; 58 } 59 const card = event.target.closest(cardSelector); 60 if (!card) return; 61 62 const link = card.querySelector(linkSelector); 63 if (!link || !link.href) return; 64 65 // Cmd/Ctrl/middle-click â new tab. Plain click â same tab. 66 if (event.metaKey || event.ctrlKey || event.shiftKey || event.button === 1) { 67 window.open(link.href, '_blank', 'noopener'); 68 } else { 69 window.location.href = link.href; 70 } 71 }); 72 73 // Keyboard parity: Enter on a focused card root navigates to the link. 74 document.addEventListener('keydown', function (event) { 75 if (event.key !== 'Enter') return; 76 if (event.target.closest(interactive)) return; 77 const card = event.target.closest(cardSelector); 78 if (!card) return; 79 const link = card.querySelector(linkSelector); 80 if (link && link.href) { 81 window.location.href = link.href; 82 } 83 }); 84 } 85 86 // Advanced search toggle 87 function initAdvancedSearch() { 88 const toggleBtn = document.getElementById('advancedToggle'); 89 const advancedOptions = document.getElementById('advancedOptions'); 90 91 if (toggleBtn && advancedOptions) { 92 toggleBtn.addEventListener('click', function() { 93 const isOpen = advancedOptions.style.display !== 'none'; 94 advancedOptions.style.display = isOpen ? 'none' : 'block'; 95 toggleBtn.classList.toggle('active'); 96 }); 97 } 98 } 99 100 // Travelers selector 101 function initTravelersSelect() { 102 const travelersSelect = document.getElementById('travelersSelect'); 103 if (!travelersSelect) return; 104 105 // Create dropdown for travelers 106 const dropdown = document.createElement('div');
107 dropdown.className = 'yatra-travelers-dropdown'; 108 dropdown.style.cssText = 'display: none; position: absolute; top: 100%; left: 0; right: 0; background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; margin-top: 4px; padding: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000;'; 109 110 dropdown.innerHTML = ` 111 <div style="margin-bottom: 12px;"> 112 <label style="display: block; font-size: 13px; font-weight: 600; color: #374151; margin-bottom: 8px;">Adults</label> 113 <div style="display: flex; align-items: center; gap: 12px;"> 114 <button type="button" class="yatra-quantity-btn" data-action="decrease" style="width: 32px; height: 32px; border: 1px solid #d1d5db; background: #fff; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center;">-</button> 115 <span class="yatra-quantity-value" style="min-width: 30px; text-align: center; font-weight: 600;">2</span> 116 <button type="button" class="yatra-quantity-btn" data-action="increase" style="width: 32px; height: 32px; border: 1px solid #d1d5db; background: #fff; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center;">+</button> 117 </div> 118 </div> 119 <button type="button" class="yatra-apply-travelers" style="width: 100%; padding: 10px; background: #3b82f6; color: #fff; border: none; border-radius: 6px; font-weight: 600; cursor: pointer;">Apply</button> 120 `; 121 122 const parent = travelersSelect.parentElement; 123 parent.style.position = 'relative'; 124 parent.appendChild(dropdown); 125 126 const input = travelersSelect.querySelector('input'); 127 let adults = 2; 128 129 travelersSelect.addEventListener('click', function(e) { 130 e.stopPropagation(); 131 dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; 132 }); 133 134 dropdown.querySelectorAll('.yatra-quantity-btn').forEach(btn => { 135 btn.addEventListener('click', function(e) { 136 e.stopPropagation(); 137 const action = this.getAttribute('data-action'); 138 if (action === 'increase') { 139 adults = Math.min(adults + 1, 20); 140 } else { 141 adults = Math.max(adults - 1, 1); 142 } 143 dropdown.querySelector('.yatra-quantity-value').textContent = adults; 144 }); 145 }); 146 147 dropdown.querySelector('.yatra-apply-travelers').addEventListener('click', function(e) { 148 e.stopPropagation(); 149 input.value = adults + ' Adult' + (adults > 1 ? 's' : ''); 150 dropdown.style.display = 'none'; 151 }); 152 153 document.addEventListener('click', function() { 154 dropdown.style.display = 'none'; 155 }); 156 157 dropdown.addEventListener('click', function(e) { 158 e.stopPropagation(); 159 }); 160 } 161 162 // View toggle (grid/list): one grid per page; localStorage key is per grid id (not global). 163 function initViewToggle() { 164 const grids = [ 165 document.getElementById('trip-grid'), 166 document.getElementById('category-grid'), 167 document.getElementById('destination-grid'), 168 document.getElementById('activity-grid') 169 ].filter(function (el) { 170 return el !== null; 171 }); 172 173 if (grids.length === 0) { 174 return; 175 } 176 177 const grid = grids[0]; 178 const storageKey = 'yatra_listing_view_' + (grid.id || 'listing'); 179 180 const viewButtons = document.querySelectorAll('.yatra-view-btn[data-view]'); 181 if (viewButtons.length === 0) { 182 return; 183 } 184 185 const listingRoot = grid.closest('.yatra-listing-page') || document.body; 186 187 function applyView(view) { 188 if (view !== 'list') { 189 view = 'grid'; 190 } 191 192 listingRoot.querySelectorAll('.yatra-results-controls .yatra-view-btn[data-view]').forEach(function (btn) { 193 const btnView = btn.getAttribute('data-view'); 194 btn.classList.toggle('active', btnView === view); 195 }); 196 197 if (view === 'list') { 198 grid.classList.add('list-view'); 199 grid.classList.remove('yatra-list-view'); 200 } else { 201 grid.classList.remove('list-view'); 202 grid.classList.remove('yatra-list-view'); 203 } 204 } 205 206 let initialView = 'grid'; 207 try { 208 const saved = window.localStorage.getItem(storageKey); 209 if (saved === 'list' || saved === 'grid') { 210 initialView = saved; 211 } 212 } catch (e) { 213 // ignore 214 } 215 216 applyView(initialView); 217 218 viewButtons.forEach(function (button) { 219 button.addEventListener('click', function () { 220 const view = this.getAttribute('data-view') || 'grid'; 221 applyView(view); 222 try { 223 window.localStorage.setItem(storageKey, view); 224 } catch (e) { 225 // ignore 226 } 227 }); 228 }); 229 } 230 231 // Price range slider - use specific IDs to avoid conflict with duration slider 232 function initPriceRange() { 233 const minRange = document.getElementById('priceRangeMin'); 234 const maxRange = document.getElementById('priceRangeMax'); 235 const priceDisplay = document.querySelector('.yatra-price-display'); 236 const minInput = document.getElementById('priceMin'); 237 const maxInput = document.getElementById('priceMax'); 238 239 if (!minRange || !maxRange || !priceDisplay) return; 240 241 function updatePriceDisplay() { 242 const min = parseInt(minRange.value) || 0; 243 const max = parseInt(maxRange.value) || 10000; 244 245 // Format with commas
246 const minFormatted = min.toLocaleString(); 247 const maxFormatted = max.toLocaleString(); 248 249 priceDisplay.textContent = `$${minFormatted} - $${maxFormatted}`; 250 251 if (minInput) minInput.value = min; 252 if (maxInput) maxInput.value = max; 253 } 254 255 minRange.addEventListener('input', function() { 256 if (parseInt(this.value) > parseInt(maxRange.value)) { 257 this.value = maxRange.value; 258 } 259 updatePriceDisplay(); 260 }); 261 262 maxRange.addEventListener('input', function() { 263 if (parseInt(this.value) < parseInt(minRange.value)) { 264 this.value = minRange.value; 265 } 266 updatePriceDisplay(); 267 }); 268 269 if (minInput) { 270 minInput.addEventListener('input', function() { 271 minRange.value = this.value || 0; 272 updatePriceDisplay(); 273 }); 274 } 275 276 if (maxInput) { 277 maxInput.addEventListener('input', function() { 278 maxRange.value = this.value || 10000; 279 updatePriceDisplay(); 280 }); 281 } 282 283 // Initial display 284 updatePriceDisplay(); 285 } 286 287 // Clear filters functionality is handled by listing-filters.js 288 // Removed conflicting event listener to prevent interference 289 290 // Horizontal Search Dropdowns 291 function initHorizontalSearchDropdowns() { 292 const dropdowns = document.querySelectorAll('.yatra-search-dropdown'); 293 294 if (dropdowns.length === 0) return; 295 296 // Close all dropdowns when clicking outside 297 document.addEventListener('click', function(e) { 298 if (!e.target.closest('.yatra-search-dropdown')) { 299 dropdowns.forEach(dropdown => { 300 dropdown.classList.remove('open'); 301 }); 302 } 303 }); 304 305 dropdowns.forEach(dropdown => { 306 const trigger = dropdown.querySelector('.yatra-dropdown-trigger'); 307 const menu = dropdown.querySelector('.yatra-dropdown-menu'); 308 const options = dropdown.querySelectorAll('.yatra-dropdown-option'); 309 const valueSpan = dropdown.querySelector('.yatra-dropdown-value'); 310 311 if (trigger) { 312 trigger.addEventListener('click', function(e) { 313 e.stopPropagation(); 314 315 // Close other dropdowns 316 dropdowns.forEach(d => { 317 if (d !== dropdown) { 318 d.classList.remove('open'); 319 } 320 }); 321 322 // Toggle current dropdown 323 dropdown.classList.toggle('open'); 324 }); 325 } 326 327 // Handle option selection 328 options.forEach(option => { 329 option.addEventListener('click', function(e) { 330 e.stopPropagation(); 331 332 // Remove selected from siblings 333 options.forEach(opt => opt.classList.remove('selected')); 334 335 // Add selected to clicked option 336 this.classList.add('selected'); 337 338 // Update the display value 339 if (valueSpan) { 340 valueSpan.textContent = this.textContent; 341 valueSpan.classList.add('selected'); 342 } 343 344 // Close dropdown 345 dropdown.classList.remove('open'); 346 }); 347 }); 348 349 // Keep dropdown open when clicking inside (for duration slider) 350 if (menu) { 351 menu.addEventListener('click', function(e) { 352 e.stopPropagation(); 353 }); 354 } 355 }); 356 } 357 358 // Duration Slider 359 function initDurationSlider() { 360 const durationDropdown = document.querySelector('[data-dropdown="duration"]'); 361 if (!durationDropdown) return; 362 363 const minSlider = durationDropdown.querySelector('#durationMin'); 364 const maxSlider = durationDropdown.querySelector('#durationMax'); 365 const minBadge = durationDropdown.querySelector('.yatra-duration-min-badge'); 366 const maxBadge = durationDropdown.querySelector('.yatra-duration-max-badge'); 367 const sliderRange = durationDropdown.querySelector('.yatra-slider-range'); 368 const valueSpan = durationDropdown.querySelector('.yatra-dropdown-value'); 369 370 if (!minSlider || !maxSlider) return; 371 372 function durationBounds() { 373 const lo = parseInt(durationDropdown.getAttribute('data-duration-min') || minSlider.min, 10); 374 const hi = parseInt(durationDropdown.getAttribute('data-duration-max') || maxSlider.max, 10); 375 const safeLo = isNaN(lo) ? 1 : lo; 376 const safeHi = isNaN(hi) || hi < safeLo ? safeLo : hi; 377 return { lo: safeLo, hi: safeHi, span: Math.max(1, safeHi - safeLo) }; 378 } 379 380 function updateDurationDisplay() { 381 let min = parseInt(minSlider.value, 10); 382 let max = parseInt(maxSlider.value, 10); 383 384 // Ensure min doesn't exceed max 385 if (min > max) { 386 [min, max] = [max, min]; 387 minSlider.value = min; 388 maxSlider.value = max; 389 } 390 391 // Update badges 392 if (minBadge) minBadge.textContent = min + ' Days'; 393 if (maxBadge) maxBadge.textContent = max + ' Days'; 394 395 // Update the dropdown value display 396 if (valueSpan) { 397 valueSpan.textContent = min + ' - ' + max + ' Days'; 398 valueSpan.classList.add('selected'); 399 } 400 401 // Update the slider range track 402 if (sliderRange) { 403 const b = durationBounds(); 404 const minPercent = ((min - b.lo) / b.span) * 100; 405 const maxPercent = ((max - b.lo) / b.span) * 100; 406 sliderRange.style.left = minPercent + '%'; 407 sliderRange.style.width = Math.max(0, maxPercent - minPercent) + '%'; 408 } 409 } 410 411 minSlider.addEventListener('input', updateDurationDisplay); 412 maxSlider.addEventListener('input', updateDurationDisplay); 413 414 // Initial display 415 updateDurationDisplay(); 416 } 417 418})(); 419
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.