PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5.1
Yatra – Travel Booking & Tour Operator Software v3.0.5.1
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / assets / js / listing.js

listing.js in Yatra – Travel Booking & Tour Operator Software 3.0.5.1, at assets/js/listing.js

420 lines 16.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
420