PageSourceSearch

https://travel.historictownsofamerica.com/wp-content/plugins/yatra/assets/js/listing.js?ver=3.0.15.1789496072

js historictownsofamerica.com collected 2026-09-25 00:02:26 UTC 16,899 bytes, 419 lines download raw bytes

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.