/** * Yatra Single Trip Page JavaScript * Class-based implementation for trip page features */ /* jshint esversion: 6 */ // Translation helpers (booking.js installs the same shims if it runs first; // idempotent setup so either load order works). (function () { if (typeof window.__ === 'function') return; if (window.wp && window.wp.i18n && typeof window.wp.i18n.__ === 'function') { window.__ = function (text, domain) { return window.wp.i18n.__(text, domain || 'yatra'); }; window._n = function (s, p, n, domain) { return window.wp.i18n._n(s, p, n, domain || 'yatra'); }; window._x = function (text, ctx, domain) { return window.wp.i18n._x(text, ctx, domain || 'yatra'); }; window.sprintf = window.sprintf || (window.wp.i18n.sprintf || function (fmt) { return fmt; }); } else { window.__ = function (text) { return text; }; window._n = function (s, p, n) { return n === 1 ? s : p; }; window._x = function (text) { return text; }; window.sprintf = window.sprintf || function (fmt) { return fmt; }; } })(); (function() { 'use strict'; /** * Convert PHP date format (WordPress/Yatra) to Flatpickr format. * Covers the common tokens exposed in Yatra Settings UI. * * @param {string} php * @returns {string} */ const phpDateFormatToFlatpickr = (php) => { if (!php || typeof php !== 'string') return 'F j, Y'; const map = { // Day d: 'd', j: 'j', D: 'D', l: 'l', // Month m: 'm', n: 'n', M: 'M', F: 'F', // Year Y: 'Y', y: 'y', // Time (for any datetime pickers) H: 'H', G: 'H', h: 'h', g: 'h', i: 'i', s: 'S', a: 'K', A: 'K' }; let out = ''; let escaping = false; for (let i = 0; i < php.length; i++) { const ch = php[i]; if (escaping) { out += ch; escaping = false; continue; } if (ch === '\\') { escaping = true; continue; } out += map[ch] || ch; } return out || 'F j, Y'; }; const getTripAltDateFormat = () => { const cfg = window.yatraTripData || {}; const php = cfg.dateFormat || cfg.date_format || 'Y-m-d'; return phpDateFormatToFlatpickr(php); }; /** * Flatpickr locale from PHP ({@see FrontendAssetsProvider::buildFlatpickrLocalePayload}). * @returns {Record} */ const getFlatpickrLocale = () => { const cfg = window.yatraTripData || {}; const l = cfg.flatpickrLocale; if (!l || typeof l !== 'object') { return { firstDayOfWeek: 1 }; } const out = Object.assign({}, l); if (typeof out.firstDayOfWeek !== 'number' || Number.isNaN(out.firstDayOfWeek)) { out.firstDayOfWeek = 1; } return out; }; const getRestBase = () => { const siteUrl = (window.yatraTripData && window.yatraTripData.siteUrl) || (window.yatraAdmin && window.yatraAdmin.siteUrl) || window.location.origin || ''; let base = (window.yatraTripData && (window.yatraTripData.apiUrl || window.yatraTripData.restUrl)) || (window.wpApiSettings && window.wpApiSettings.root) || `${siteUrl.replace(/\/$/, '')}/wp-json`; base = base.replace(/\/$/, ''); // Support plain permalinks via rest_route when pretty permalinks are off const permalinkStructure = (window.yatraTripData && window.yatraTripData.permalinkStructure) || (window.yatraAdmin && window.yatraAdmin.permalinkStructure) || ''; const isPlain = permalinkStructure === 'plain' || !permalinkStructure; return { base, isPlain }; }; /** * YYYY-MM from a calendar day; server maps to its locale M-Y to match card data-month. * @param {string} dateStr YYYY-MM-DD */ const monthFilterKeyFromIsoDate = (dateStr) => { if (!dateStr || typeof dateStr !== 'string') { return 'all'; } const m = dateStr.match(/^(\d{4})-(\d{2})-\d{2}/); if (!m) { return 'all'; } return `${m[1]}-${m[2]}`; }; /** * Build public REST URL for trip availability HTML (full section or card fragment). * @param {string|number} tripId * @param {{date?: string, month_filter?: string, sort?: string, page?: number, per_page?: number, partial?: boolean}} params */ const buildAvailabilityTemplateUrl = (tripId, params = {}) => { const { base: baseUrl, isPlain } = getRestBase(); const q = new URLSearchParams(); if (params.date) { q.set('date', params.date); } const mf = params.month_filter != null && params.month_filter !== '' ? params.month_filter : 'all'; q.set('month_filter', mf); q.set('sort', params.sort || 'date-asc'); q.set('page', String(Math.max(1, parseInt(params.page, 10) || 1))); q.set('per_page', String(Math.min(50, Math.max(1, parseInt(params.per_page, 10) || 10)))); if (params.partial) { q.set('partial', '1'); } const qs = q.toString(); if (isPlain && baseUrl.includes('rest_route=')) { return `${baseUrl}/trips/${tripId}/availability-template&${qs}`; } if (baseUrl.includes('/yatra/v1')) { return `${baseUrl}/trips/${tripId}/availability-template?${qs}`; } return `${baseUrl}/yatra/v1/trips/${tripId}/availability-template?${qs}`; }; // Currency formatting helper for trip page (uses localized yatraTripData) if (typeof window.yatra_format_price_js !== 'function') { window.yatra_format_price_js = function(amount) { const cfg = window.yatraTripData || {}; const symbol = (typeof cfg.currencySymbol === 'string' && cfg.currencySymbol.length) ? cfg.currencySymbol : (typeof cfg.currency === 'string' ? cfg.currency : '$'); const position = ( cfg.currencyPosition || cfg.currency_position || 'before' ).toString().toLowerCase().trim(); const decimals = Number.isFinite(Number(cfg.decimalPlaces)) ? Number(cfg.decimalPlaces) : 2; const thousandSep = (typeof cfg.thousandSeparator === 'string') ? cfg.thousandSeparator : ','; const decimalSep = (typeof cfg.decimalSeparator === 'string') ? cfg.decimalSeparator : '.'; const num = Number(amount) || 0; const formatted = new Intl.NumberFormat(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }).format(num) .replace(/,/g, 'TEMP_THOUSAND') .replace(/\./g, 'TEMP_DECIMAL') .replace(/TEMP_THOUSAND/g, thousandSep) .replace(/TEMP_DECIMAL/g, decimalSep); var pos = position; if (pos === 'before') { pos = 'left_space'; } else if (pos === 'after') { pos = 'right_space'; } if (pos === 'right') { return formatted + symbol; } if (pos === 'right_space') { return formatted + ' ' + symbol; } if (pos === 'left') { return symbol + formatted; } return symbol + ' ' + formatted; }; } /** * Resolve the real image URL from an element. * * Caching / performance plugins (LiteSpeed Cache, WP Rocket, W3 Total Cache, * Autoptimize, etc.) replace the `src` attribute with a tiny base64 * placeholder and store the real URL in a data-* attribute. We check our * own `data-yatra-src` first (added in PHP templates and never touched by * any third-party plugin), then fall back to the most common lazy-load * attribute names, and finally fall back to `img.src` while filtering out * obvious placeholders so we never pass a base64 GIF to the modal. */ function getImageSrc(img) { // 1. Our own reliable attribute – always present on Yatra gallery images. const yatra = img.getAttribute('data-yatra-src'); if (yatra) return yatra; // 2. Common lazy-load attributes used by popular caching plugins. const lazyAttrs = [ 'data-src', // LiteSpeed Cache, WP Rocket, Smush, BJ Lazy Load 'data-lazyload', // LiteSpeed Cache (older attribute) 'data-lazy-src', // WordPress core lazy-load 'data-original', // jQuery.lazyLoad plugin 'data-pagespeed-lazy-src', // Google PageSpeed / mod_pagespeed 'data-lazy', // Jetpack 'data-full-size', // Yatra itinerary images ]; for (const attr of lazyAttrs) { const val = img.getAttribute(attr); if (val && !val.startsWith('data:')) return val; } // 3. Native src – only use if it is not a placeholder. const src = img.src || ''; if (src && !src.startsWith('data:')) return src; return ''; } /** * Gallery Modal Class * Handles image gallery modal functionality */ class GalleryModal { constructor() { this.modal = null; this.images = []; this.currentIndex = 0; this.init(); } init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } } setup() { // Wait for DOM to be ready if needed if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.initializeGallery()); } else { this.initializeGallery(); } } initializeGallery() { this.modal = document.getElementById('hero-gallery'); if (!this.modal) { console.warn('Hero gallery modal not found'); return; } this.collectImages(); this.createThumbnails(); this.attachEventListeners(); this.attachItineraryVideoListeners(); } collectImages() { this.images = []; // Helper: deduplicate by resolved URL. const add = (img, fallbackAlt) => { const src = getImageSrc(img); if (!src) return; if (!this.images.some(i => i.src === src)) { this.images.push({ src, alt: img.alt || fallbackAlt }); } }; // 1. Prefer the JSON media-data blob (never touched by caching plugins). const mediaScript = document.getElementById('yatra-hero-media-data'); if (mediaScript) { try { const mediaData = JSON.parse(mediaScript.textContent || '{}'); const imageUrls = Array.isArray(mediaData.images) ? mediaData.images : []; imageUrls.forEach((url, index) => { if (url && typeof url === 'string' && !this.images.some(i => i.src === url)) { this.images.push({ src: url, // Indexed alt text — sprintf isn't guaranteed // available on every page, so we replace() the // %d placeholder ourselves. Keeps the msgid a // literal so makepot extracts cleanly. /* translators: %d is the 1-based image index in the gallery. */ alt: __('Gallery Image %d', 'yatra').replace('%d', String(index + 1)), }); } }); } catch (e) { // JSON parse error – fall through to DOM-based collection. } } // 2. Collect from hero slides (covers any images not in the JSON blob). document.querySelectorAll('.yatra-trip-hero-slide img').forEach((img, index) => { /* translators: %d is the 1-based image index in the gallery. */ add(img, __('Gallery Image %d', 'yatra').replace('%d', String(index + 1))); }); // 3. Collect from gallery section (hero gallery only, NOT itinerary). document.querySelectorAll('.yatra-trip-gallery .yatra-gallery-item img').forEach((img) => { add(img, __('Gallery Image', 'yatra')); }); // 4. Collect from hero main image. const heroMainImg = document.querySelector('.yatra-hero-main-img'); if (heroMainImg) add(heroMainImg, __('Main Image', 'yatra')); // 5. Collect from side images. document.querySelectorAll('.yatra-side-image-item img').forEach((img) => { add(img, __('Side Image', 'yatra')); }); } createThumbnails() { const container = this.modal.querySelector('.yatra-gallery-thumbnails-container'); if (!container) return; container.innerHTML = ''; const totalCount = this.modal.querySelector('.yatra-gallery-total-count'); if (totalCount) { totalCount.textContent = this.images.length; } this.images.forEach((img, index) => { const thumbnail = document.createElement('div'); thumbnail.className = 'yatra-gallery-thumbnail'; thumbnail.setAttribute('data-index', index); if (index === 0) thumbnail.classList.add('active'); const thumbnailImg = document.createElement('img'); thumbnailImg.src = img.src; thumbnailImg.alt = img.alt; thumbnail.appendChild(thumbnailImg); thumbnail.addEventListener('click', () => this.showImage(index)); container.appendChild(thumbnail); }); } showImage(index) { if (index < 0 || index >= this.images.length) return; this.currentIndex = index; const image = this.images[index]; const modalImage = this.modal.querySelector('.yatra-gallery-modal-image'); const modalLoader = this.modal.querySelector('.yatra-gallery-modal-loader'); const currentIndexSpan = this.modal.querySelector('.yatra-gallery-current-index'); const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev'); const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next'); if (currentIndexSpan) { currentIndexSpan.textContent = index + 1; } // Update thumbnails const thumbnails = this.modal.querySelectorAll('.yatra-gallery-thumbnail'); thumbnails.forEach((thumb, i) => { thumb.classList.toggle('active', i === index); }); // Show loader if (modalLoader) modalLoader.classList.add('active'); if (modalImage) modalImage.classList.remove('loaded'); // Load image const img = new Image(); img.onload = () => { if (modalImage) { modalImage.src = image.src; modalImage.alt = image.alt; modalImage.classList.add('loaded'); } if (modalLoader) modalLoader.classList.remove('active'); }; img.onerror = () => { if (modalLoader) modalLoader.classList.remove('active'); if (modalImage) modalImage.src = image.src; }; img.src = image.src; // Update button states if (prevBtn) { prevBtn.style.opacity = index === 0 ? '0.5' : '1'; prevBtn.style.pointerEvents = index === 0 ? 'none' : 'auto'; } if (nextBtn) { nextBtn.style.opacity = index === this.images.length - 1 ? '0.5' : '1'; nextBtn.style.pointerEvents = index === this.images.length - 1 ? 'none' : 'auto'; } } open(startIndex = 0) { if (this.images.length === 0) return; this.modal.classList.add('active'); document.body.style.overflow = 'hidden'; this.showImage(startIndex); } close() { this.modal.classList.remove('active'); document.body.style.overflow = ''; } next() { if (this.currentIndex < this.images.length - 1) { this.showImage(this.currentIndex + 1); } } prev() { if (this.currentIndex > 0) { this.showImage(this.currentIndex - 1); } } attachItineraryVideoListeners() { // This method is called from setup() but the actual event listeners // are handled in the inline script in content-itinerary.php // This is just a placeholder to prevent errors } attachEventListeners() { // Close button const closeBtn = this.modal.querySelector('.yatra-gallery-modal-close'); if (closeBtn) { closeBtn.addEventListener('click', () => this.close()); } // Overlay const overlay = this.modal.querySelector('.yatra-gallery-modal-overlay'); if (overlay) { overlay.addEventListener('click', () => this.close()); } // Navigation buttons const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev'); const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next'); if (prevBtn) prevBtn.addEventListener('click', () => this.prev()); if (nextBtn) nextBtn.addEventListener('click', () => this.next()); // Keyboard navigation document.addEventListener('keydown', (e) => { if (!this.modal.classList.contains('active')) return; if (e.key === 'Escape') this.close(); if (e.key === 'ArrowLeft') this.prev(); if (e.key === 'ArrowRight') this.next(); }); // Open from play button const playButton = document.querySelector('.yatra-hero-gallery-play'); const galleryPlayBtn = document.querySelector('.yatra-gallery-play-btn'); if (playButton) { playButton.addEventListener('click', () => this.open(0)); } if (galleryPlayBtn) { galleryPlayBtn.addEventListener('click', () => this.open(0)); } // Open from gallery items const galleryItems = document.querySelectorAll('.yatra-trip-gallery .yatra-gallery-item'); galleryItems.forEach((item, index) => { item.addEventListener('click', () => { const img = item.querySelector('img'); if (img) { const src = getImageSrc(img); const imageIndex = this.images.findIndex(i => i.src === src); this.open(imageIndex >= 0 ? imageIndex : index); } }); }); // Open from side images const sideImages = document.querySelectorAll('.yatra-side-image-item'); sideImages.forEach((item) => { item.addEventListener('click', (e) => { if (e.target.closest('.yatra-favorite-btn') || e.target.closest('.yatra-view-all-photos-btn')) { return; } const img = item.querySelector('img'); if (img) { const src = getImageSrc(img); const imageIndex = this.images.findIndex(i => i.src === src); this.open(imageIndex >= 0 ? imageIndex : 0); } }); }); // Open from main image link const mainImageLink = document.querySelector('.yatra-hero-main-img-link'); if (mainImageLink) { mainImageLink.addEventListener('click', (e) => { // Check if gallery should be prevented (for non-image media types) if (mainImageLink.getAttribute('data-prevent-gallery') === 'true') { // Don't prevent default - let the overlay handle the click return; } e.preventDefault(); this.open(0); // Open with featured image as first image }); } } } /** * Itinerary Gallery Modal Class * Separate gallery modal for itinerary section - independent from hero gallery. * * Scoping: a single modal element is reused, but the image set is RECOMPUTED on * every click from the *closest* `.yatra-entry-gallery` ancestor. Clicking a * thumbnail in Day 2 must open only Day 2's images (starting at the clicked one) — * never the global pool of every day's images. The hidden * `.yatra-itinerary-trip-gallery` div on the page is for SEO / structured data * only; it intentionally no longer drives the modal. */ class ItineraryGalleryModal { constructor() { this.modal = null; this.images = []; // currently-visible image set (the active scope) this.currentIndex = 0; this.init(); } init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } } setup() { this.modal = document.getElementById('itinerary-gallery'); if (!this.modal) { console.warn('Itinerary gallery modal not found'); return; } this.attachEventListeners(); } /** * Build the image list from a scope element (an `.yatra-entry-gallery` block). * Falls back to the document if no scope is given (preserves the legacy * "global pool" behaviour for the SEO-only thumbnails clicker). */ collectImagesFrom(scope) { const root = scope || document; const images = []; const add = (img, fallbackAlt) => { const src = img.getAttribute('data-full-size') || getImageSrc(img); if (!src) return; if (!images.some(i => i.src === src)) { images.push({ src, alt: img.alt || fallbackAlt }); } }; // Visible gallery thumbnails inside the scope root.querySelectorAll('.yatra-media-card img').forEach((img) => { add(img, sprintf( /* translators: %d: gallery image sequence number. */ __('Itinerary Gallery Image %d', 'yatra'), images.length + 1 )); }); // Video links inside the scope (fallback selector) root.querySelectorAll('.yatra-itinerary-video-link img').forEach((img) => { add(img, sprintf( /* translators: %d: itinerary video sequence number. */ __('Itinerary Video %d', 'yatra'), images.length + 1 )); }); // If still empty AND we were given no scope, fall back to the hidden global pool // so legacy click handlers on `.yatra-itinerary-trip-gallery .yatra-gallery-item` // continue to function. if (images.length === 0 && !scope) { document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item img').forEach((img) => { add(img, sprintf( /* translators: %d: itinerary image sequence number. */ __('Itinerary Image %d', 'yatra'), images.length + 1 )); }); } return images; } /** Replace the active image set and rebuild thumbnails. */ setActiveImages(images) { this.images = images; this.currentIndex = 0; this.createThumbnails(); } createThumbnails() { const container = this.modal.querySelector('.yatra-gallery-thumbnails-container'); if (!container) return; container.innerHTML = ''; const totalCount = this.modal.querySelector('.yatra-gallery-total-count'); if (totalCount) { totalCount.textContent = this.images.length; } this.images.forEach((image, index) => { const thumb = document.createElement('div'); thumb.className = 'yatra-gallery-thumbnail'; thumb.innerHTML = `${image.alt}`; thumb.addEventListener('click', () => this.showImage(index)); container.appendChild(thumb); }); } showImage(index) { if (index < 0 || index >= this.images.length) return; this.currentIndex = index; const image = this.images[index]; const modalImage = this.modal.querySelector('.yatra-gallery-modal-image'); const modalLoader = this.modal.querySelector('.yatra-gallery-modal-loader'); const currentIndexSpan = this.modal.querySelector('.yatra-gallery-current-index'); const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev'); const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next'); if (currentIndexSpan) { currentIndexSpan.textContent = index + 1; } // Show loader if (modalLoader) modalLoader.classList.add('active'); if (modalImage) modalImage.classList.remove('loaded'); // Load image const img = new Image(); img.onload = () => { if (modalImage) { modalImage.src = image.src; modalImage.alt = image.alt; modalImage.classList.add('loaded'); } if (modalLoader) modalLoader.classList.remove('active'); }; img.onerror = () => { if (modalLoader) modalLoader.classList.remove('active'); if (modalImage) modalImage.src = image.src; }; img.src = image.src; // Update button states if (prevBtn) { prevBtn.style.opacity = index === 0 ? '0.5' : '1'; prevBtn.style.pointerEvents = index === 0 ? 'none' : 'auto'; } if (nextBtn) { nextBtn.style.opacity = index === this.images.length - 1 ? '0.5' : '1'; nextBtn.style.pointerEvents = index === this.images.length - 1 ? 'none' : 'auto'; } // Update thumbnails const thumbnails = this.modal.querySelectorAll('.yatra-gallery-thumbnail'); thumbnails.forEach((thumb, i) => { thumb.classList.toggle('active', i === index); }); } open(startIndex = 0) { if (this.images.length === 0) return; this.modal.classList.add('active'); document.body.style.overflow = 'hidden'; this.showImage(startIndex); } close() { this.modal.classList.remove('active'); document.body.style.overflow = ''; } next() { if (this.currentIndex < this.images.length - 1) { this.showImage(this.currentIndex + 1); } } prev() { if (this.currentIndex > 0) { this.showImage(this.currentIndex - 1); } } attachEventListeners() { // Close button const closeBtn = this.modal.querySelector('.yatra-gallery-modal-close'); if (closeBtn) { closeBtn.addEventListener('click', () => this.close()); } // Overlay const overlay = this.modal.querySelector('.yatra-gallery-modal-overlay'); if (overlay) { overlay.addEventListener('click', () => this.close()); } // Navigation buttons const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev'); const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next'); if (prevBtn) prevBtn.addEventListener('click', () => this.prev()); if (nextBtn) nextBtn.addEventListener('click', () => this.next()); // Keyboard navigation document.addEventListener('keydown', (e) => { if (!this.modal.classList.contains('active')) return; if (e.key === 'Escape') this.close(); if (e.key === 'ArrowLeft') this.prev(); if (e.key === 'ArrowRight') this.next(); }); // Itinerary day gallery thumbnails — SCOPED to the clicked day. // Every click recomputes the image set from the closest .yatra-entry-gallery so // Day 2's modal only ever shows Day 2's photos, starting at the clicked one. // Video links are intentionally NOT captured here — they have their own handler // that opens YatraVideoPlayer (see further down in this file). const onItineraryClick = (e) => { const link = e.target.closest('.yatra-itinerary-gallery-link'); if (!link) return; // Don't hijack video link clicks even if they happen to match if (link.classList.contains('yatra-itinerary-video-link')) return; const scope = link.closest('.yatra-entry-gallery'); if (!scope) return; e.preventDefault(); const scopedImages = this.collectImagesFrom(scope); if (scopedImages.length === 0) return; // Find the clicked image's index within the scoped set const clickedImg = link.querySelector('img'); const clickedSrc = clickedImg ? (clickedImg.getAttribute('data-full-size') || getImageSrc(clickedImg)) : null; const startIndex = clickedSrc ? Math.max(0, scopedImages.findIndex((i) => i.src === clickedSrc)) : 0; this.setActiveImages(scopedImages); this.open(startIndex); }; // Delegated listener — works even if entries are added/removed dynamically document.addEventListener('click', onItineraryClick); // Legacy: clicks on the hidden SEO pool (kept for back-compat). These open // the full pool because there's no per-day scope on the SEO container. const galleryItems = document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item'); galleryItems.forEach((item, index) => { item.addEventListener('click', () => { const allImages = this.collectImagesFrom(null); // fall back to global if (allImages.length === 0) return; const img = item.querySelector('img'); const src = img ? (img.getAttribute('data-full-size') || getImageSrc(img)) : null; const startIndex = src ? Math.max(0, allImages.findIndex((i) => i.src === src)) : index; this.setActiveImages(allImages); this.open(startIndex); }); }); } } /** * Hero Media Switcher Class * Handles media type switching in the hero section */ class HeroMediaSwitcher { constructor() { this.currentMediaType = 'images'; this.mediaData = null; this.init(); } init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } } setup() { // Get media data from JSON script const mediaDataScript = document.getElementById('yatra-hero-media-data'); if (!mediaDataScript) return; try { this.mediaData = JSON.parse(mediaDataScript.textContent); } catch (e) { console.error('Error parsing media data:', e); return; } this.attachEventListeners(); } attachEventListeners() { // Media switcher buttons const switcherBtns = document.querySelectorAll('.yatra-media-switcher-btn'); switcherBtns.forEach(btn => { btn.addEventListener('click', (e) => { const mediaType = e.currentTarget.dataset.media; this.switchMediaType(mediaType); }); }); // Media badges click const mediaBadges = document.querySelectorAll('.yatra-media-badge'); mediaBadges.forEach(badge => { badge.addEventListener('click', (e) => { const badgeType = e.currentTarget.classList.contains('yatra-media-badge-image') ? 'images' : e.currentTarget.classList.contains('yatra-media-badge-video') ? 'videos' : e.currentTarget.classList.contains('yatra-media-badge-youtube') ? 'youtube' : e.currentTarget.classList.contains('yatra-media-badge-tour') ? 'tours' : 'documents'; this.switchMediaType(badgeType); }); }); } switchMediaType(mediaType) { if (this.currentMediaType === mediaType) return; this.currentMediaType = mediaType; this.updateSwitcherButtons(); this.updateMediaBadgesVisibility(); this.updateMainContent(); this.updateSideContent(); this.updateSideImagesVisibility(); } updateSwitcherButtons() { const switcherBtns = document.querySelectorAll('.yatra-media-switcher-btn'); switcherBtns.forEach(btn => { if (btn.dataset.media === this.currentMediaType) { btn.classList.add('yatra-media-switcher-active'); } else { btn.classList.remove('yatra-media-switcher-active'); } }); } updateMediaBadgesVisibility() { const badges = { image: document.querySelector('.yatra-media-badge-image'), video: document.querySelector('.yatra-media-badge-video'), youtube: document.querySelector('.yatra-media-badge-youtube'), tour: document.querySelector('.yatra-media-badge-tour'), document: document.querySelector('.yatra-media-badge-document') }; // Hide all badges first Object.values(badges).forEach(badge => { if (badge) badge.style.display = 'none'; }); // Show only the active media type badge switch (this.currentMediaType) { case 'images': if (badges.image) badges.image.style.display = 'inline-flex'; break; case 'videos': if (badges.video) badges.video.style.display = 'inline-flex'; break; case 'youtube': if (badges.youtube) badges.youtube.style.display = 'inline-flex'; break; case 'tours': if (badges.tour) badges.tour.style.display = 'inline-flex'; break; case 'documents': if (badges.document) badges.document.style.display = 'inline-flex'; break; } } updateMainContent() { const mainImageLink = document.querySelector('.yatra-hero-main-img-link'); const mainImage = document.querySelector('.yatra-hero-main-img'); if (!mainImage) return; switch (this.currentMediaType) { case 'videos': this.showVideoContent(mainImageLink, mainImage); break; case 'youtube': this.showYoutubeContent(mainImageLink, mainImage); break; case 'tours': this.showTourContent(mainImageLink, mainImage); break; case 'documents': this.showDocumentContent(mainImageLink, mainImage); break; default: // images this.showImageContent(mainImageLink, mainImage); } } showImageContent(link, img) { const images = this.mediaData.images || []; // Remove any existing overlays (YouTube, tour, document, etc.) const container = img.parentElement; if (container) { const existingOverlay = container.querySelector('.yatra-media-overlay'); if (existingOverlay) { existingOverlay.remove(); } } if (images.length > 0) { img.src = images[0]; img.alt = __('Trip Image 1', 'yatra'); if (link) { link.href = '#'; link.dataset.imageIndex = '0'; } } } showVideoContent(link, img) { const videos = this.mediaData.videos || []; // Remove any existing overlays from other media types const container = img.parentElement; if (container) { const existingOverlay = container.querySelector('.yatra-media-overlay'); if (existingOverlay) { existingOverlay.remove(); } } if (videos.length > 0) { const video = videos[0]; // Show video thumbnail img.src = video.thumbnail || img.src; // Keep current image if no thumbnail img.alt = video.title || 'Trip Video'; // Add play button overlay this.addPlayButtonOverlay(img.parentElement); if (link) { link.href = video.url || '#'; link.dataset.videoUrl = video.url; link.dataset.videoTitle = video.title || ''; // Prevent link from opening in new tab link.addEventListener('click', (e) => { e.preventDefault(); return false; }); } } } showYoutubeContent(link, img) { const youtubeVideos = this.mediaData.youtube_videos || []; if (youtubeVideos.length > 0) { const video = youtubeVideos[0]; // Prevent link from opening in new tab if (link) { link.addEventListener('click', (e) => { e.preventDefault(); return false; }); } // Show YouTube thumbnail img.src = video.thumbnail || img.src; // Keep current image if no thumbnail img.alt = video.title || 'YouTube Video'; // Add YouTube play button overlay this.addYoutubePlayButtonOverlay(img.parentElement, video); if (link) { link.href = video.url || '#'; link.dataset.youtubeUrl = video.url; link.dataset.youtubeVideoId = video.video_id || ''; link.dataset.youtubeTitle = video.title || ''; link.dataset.embedUrl = video.embed_url || ''; } } else { console.warn('No YouTube videos available'); } } showTourContent(link, img) { const tours = this.mediaData.virtual_tours || []; if (tours.length > 0) { const tour = tours[0]; // Prevent link from opening in new tab if (link) { link.addEventListener('click', (e) => { e.preventDefault(); return false; }); } // Show tour thumbnail or keep current image if (tour.thumbnail) { img.src = tour.thumbnail; } img.alt = tour.title || '360° Virtual Tour'; // Add 360° tour overlay this.addTourOverlay(img.parentElement, tour); if (link) { link.href = tour.url || '#'; link.dataset.tourUrl = tour.url; link.dataset.tourTitle = tour.title || ''; link.dataset.tourType = tour.tour_type || '360'; link.dataset.isEmbeddable = tour.is_embeddable || 'false'; } } } showDocumentContent(link, img) { const documents = this.mediaData.documents || []; if (documents.length > 0) { const doc = documents[0]; // Keep current image for document display img.alt = doc.title || 'Trip Document'; // Add document icon overlay this.addDocumentIconOverlay(img.parentElement, doc); if (link) { link.href = doc.url || '#'; link.dataset.documentUrl = doc.url; link.dataset.documentTitle = doc.title || ''; link.dataset.downloadable = doc.is_downloadable || 'false'; } } } addPlayButtonOverlay(container) { // Remove existing overlays const existingOverlay = container.querySelector('.yatra-media-overlay'); if (existingOverlay) existingOverlay.remove(); const overlay = document.createElement('div'); overlay.className = 'yatra-media-overlay yatra-media-play-overlay'; overlay.innerHTML = `
`; // Add click handler to play video using YatraVideoPlayer module overlay.addEventListener('click', (e) => { // Only handle real user clicks, not programmatic ones if (!e.isTrusted) { return; } e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); // Get the video data from the current media type const videos = this.mediaData.videos || []; if (videos.length > 0 && window.YatraVideoPlayer) { window.YatraVideoPlayer.play(videos[0]); } else { console.error('Cannot play video - missing video data or YatraVideoPlayer', { hasVideo: videos.length > 0, hasPlayer: !!window.YatraVideoPlayer }); } }, { capture: false }); container.appendChild(overlay); } addYoutubePlayButtonOverlay(container, video) { // Remove existing overlays const existingOverlay = container.querySelector('.yatra-media-overlay'); if (existingOverlay) existingOverlay.remove(); const overlay = document.createElement('div'); overlay.className = 'yatra-media-overlay yatra-media-youtube-overlay'; overlay.innerHTML = `
`; // Add click handler to open YouTube video using YatraVideoPlayer module overlay.addEventListener('click', (e) => { // Only handle real user clicks, not programmatic ones if (!e.isTrusted) { return; } e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); if (video.url && window.YatraVideoPlayer) { window.YatraVideoPlayer.play(video); } else { console.error('Cannot play video - missing url or YatraVideoPlayer', { hasUrl: !!video.url, hasPlayer: !!window.YatraVideoPlayer }); } }, { capture: false }); container.appendChild(overlay); } addTourOverlay(container, tour) { // Remove existing overlays const existingOverlay = container.querySelector('.yatra-media-overlay'); if (existingOverlay) existingOverlay.remove(); const overlay = document.createElement('div'); overlay.className = 'yatra-media-overlay yatra-media-tour-overlay'; const tourFallbackTitle = __('360° Virtual Tour', 'yatra'); /* translators: %s: tour type (e.g. "360°"). */ const tourTypeLabel = sprintf(__('%s Experience', 'yatra'), tour.tour_type || '360°'); const tourActionLabel = __('Click to explore', 'yatra'); overlay.innerHTML = `
360°
${tour.title || tourFallbackTitle}
${tourTypeLabel}
${tourActionLabel}
`; // Add click handler to open virtual tour using YatraTourViewer module overlay.addEventListener('click', (e) => { // Only handle real user clicks, not programmatic ones if (!e.isTrusted) { return; } e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); if (tour.url && window.YatraTourViewer) { window.YatraTourViewer.view(tour); } else { console.error('Cannot view tour - missing url or YatraTourViewer', { hasUrl: !!tour.url, hasViewer: !!window.YatraTourViewer }); } }, { capture: false }); container.appendChild(overlay); } addDocumentIconOverlay(container, doc) { // Remove existing overlays const existingOverlay = container.querySelector('.yatra-media-overlay'); if (existingOverlay) existingOverlay.remove(); const overlay = document.createElement('div'); overlay.className = 'yatra-media-overlay yatra-media-document-overlay'; const documentFallbackTitle = __('Document', 'yatra'); overlay.innerHTML = `
${doc.title || documentFallbackTitle}
${this.formatFileSize(doc.file_size || 0)}
`; container.appendChild(overlay); } formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } updateSideContent() { const sideImages = document.querySelectorAll('.yatra-side-image-item'); switch (this.currentMediaType) { case 'videos': this.updateSideVideos(sideImages); break; case 'youtube': this.updateSideYoutubeVideos(sideImages); break; case 'tours': this.updateSideTours(sideImages); break; case 'documents': this.updateSideDocuments(sideImages); break; default: // images this.updateSideImages(sideImages); } } updateSideImages(sideImages) { const images = this.mediaData.images || []; sideImages.forEach((item, index) => { const img = item.querySelector('img'); if (img && images[index + 1]) { img.src = images[index + 1]; /* translators: %d: image sequence number in the trip gallery. */ img.alt = sprintf(__('Trip Image %d', 'yatra'), index + 2); } }); } updateSideVideos(sideImages) { const videos = this.mediaData.videos || []; const fallbackVideoTitle = __('Trip Video', 'yatra'); sideImages.forEach((item, index) => { const img = item.querySelector('img'); if (img && videos[index + 1]) { const video = videos[index + 1]; if (video.thumbnail) { img.src = video.thumbnail; } img.alt = video.title || fallbackVideoTitle; } }); } updateSideYoutubeVideos(sideImages) { const youtubeVideos = this.mediaData.youtube_videos || []; const fallbackYoutubeTitle = __('YouTube Video', 'yatra'); sideImages.forEach((item, index) => { const img = item.querySelector('img'); if (img && youtubeVideos[index + 1]) { const video = youtubeVideos[index + 1]; if (video.thumbnail) { img.src = video.thumbnail; } img.alt = video.title || fallbackYoutubeTitle; } }); } updateSideTours(sideImages) { const virtualTours = this.mediaData.virtual_tours || []; const fallbackTourTitle = __('360° Virtual Tour', 'yatra'); sideImages.forEach((item, index) => { const img = item.querySelector('img'); if (img && virtualTours[index + 1]) { const tour = virtualTours[index + 1]; if (tour.thumbnail) { img.src = tour.thumbnail; } img.alt = tour.title || fallbackTourTitle; } }); } updateSideDocuments(sideImages) { const documents = this.mediaData.documents || []; const fallbackDocumentTitle = __('Trip Document', 'yatra'); sideImages.forEach((item, index) => { const img = item.querySelector('img'); if (img && documents[index + 1]) { const doc = documents[index + 1]; // Keep existing image for documents img.alt = doc.title || fallbackDocumentTitle; } }); } updateSideImagesVisibility() { const sideImagesContainer = document.querySelector('.yatra-hero-side-images'); const heroImagesContainer = document.querySelector('.yatra-hero-images'); const mainImageLink = document.querySelector('.yatra-hero-main-img-link'); if (!sideImagesContainer) return; // Hide side images for non-image media types and make main image full width if (this.currentMediaType === 'images') { sideImagesContainer.classList.remove('yatra-media-non-image'); if (heroImagesContainer) { heroImagesContainer.classList.remove('yatra-media-full-width'); } // Re-enable gallery click for images if (mainImageLink) { mainImageLink.removeAttribute('data-prevent-gallery'); } } else { sideImagesContainer.classList.add('yatra-media-non-image'); if (heroImagesContainer) { heroImagesContainer.classList.add('yatra-media-full-width'); } // Mark to prevent gallery opening for non-image media if (mainImageLink) { mainImageLink.setAttribute('data-prevent-gallery', 'true'); } } } } /** * Booking Sidebar Class * Handles booking form interactions (Check Availability & Make Enquiry buttons) * Also handles date picker and travelers selector for main booking form */ class BookingSidebar { constructor() { this.checkAvailabilityBtn = null; this.makeEnquiryBtn = null; this.heroBookNowBtn = null; this.dateInput = null; this.participantsSelect = null; this.datepickerInstance = null; this.init(); } init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } } setup() { this.checkAvailabilityBtn = document.getElementById('check-availability-btn'); this.makeEnquiryBtn = document.getElementById('open-enquiry-modal'); this.heroBookNowBtn = document.getElementById('yatra-hero-book-now-btn'); this.dateInput = document.getElementById('travel_date'); this.participantsSelect = document.querySelector('.yatra-participants-select'); if (!this.checkAvailabilityBtn) { console.warn('Check Availability button not found'); } this.attachEventListeners(); this.initDateField(); this.initTravelersField(); } attachEventListeners() { // Check Availability button if (this.checkAvailabilityBtn) { this.checkAvailabilityBtn.addEventListener('click', (e) => { e.preventDefault(); this.handleCheckAvailability(); }); } // Hero "Book Now" (should behave like Check Availability) if (this.heroBookNowBtn) { this.heroBookNowBtn.addEventListener('click', (e) => { e.preventDefault(); const disabled = this.heroBookNowBtn.classList.contains('is-disabled') || this.heroBookNowBtn.getAttribute('aria-disabled') === 'true'; if (disabled) return; const bookingSidebar = document.getElementById('booking') || document.querySelector('.yatra-trip-sidebar'); if (bookingSidebar && bookingSidebar.scrollIntoView) { bookingSidebar.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // Trigger the same availability flow if (this.checkAvailabilityBtn) { this.handleCheckAvailability(); } }); } // Make Enquiry button if (this.makeEnquiryBtn) { this.makeEnquiryBtn.addEventListener('click', (e) => { e.preventDefault(); this.handleMakeEnquiry(); }); } } initDateField() { if (!this.dateInput) return; // Pro "date-as-dropdown" mode renders a