PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 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 All 84 releases
← All changes | assets/js/single-trip.js +263 -73 3.0.2.6 → 3.0.16 View file →
@@ -9,53 +9,232 @@
9 9 *
10 10 * @package Yatra
11 11 */
12 12
13 +// Translation helpers — idempotent shim matching booking.js / trip.js so
14 +// `__()` resolves through wp.i18n when wp-i18n is enqueued, otherwise it
15 +// falls back to the source string.
16 +// Parse a numeric min/max attribute while preserving a legitimate 0.
17 +// `parseInt(el.getAttribute('max')) || 99` treated max="0" — what a sold-out
18 +// departure renders — as "unset" and fell back to the placeholder ceiling, so
19 +// the guest selector stayed usable and ignored the departure's real capacity.
20 +// Only an absent or non-numeric value may fall back. Installed once and shared
21 +// by every frontend script, so load order does not matter.
22 +if (typeof window.yatraNumOr !== 'function') {
23 + window.yatraNumOr = function (raw, fallback) {
24 + var parsed = parseInt(raw, 10);
25 + return isNaN(parsed) ? fallback : parsed;
26 + };
27 +}
28 +
29 +(function () {
30 + var i18n = (window.wp && window.wp.i18n) ? window.wp.i18n : null;
31 + if (i18n && typeof i18n.__ === 'function') {
32 + window.__ = function (text, domain) { return i18n.__(text, domain || 'yatra'); };
33 + window._n = function (s, p, n, domain) { return i18n._n(s, p, n, domain || 'yatra'); };
34 + window._x = function (text, ctx, domain) { return i18n._x(text, ctx, domain || 'yatra'); };
35 + // Always bind WordPress's own sprintf (it substitutes %d/%s/%1$s).
36 + // Do NOT defer to a pre-existing window.sprintf: a theme or other
37 + // plugin can define an incompatible global that leaves placeholders
38 + // unsubstituted (the cause of literal "%d"/"%s" in the UI).
39 + if (typeof i18n.sprintf === 'function') {
40 + window.sprintf = function () { return i18n.sprintf.apply(i18n, arguments); };
41 + } else if (typeof window.sprintf !== 'function') {
42 + window.sprintf = function (fmt) { return fmt; };
43 + }
44 + } else {
45 + // wp.i18n not present — install passthroughs only if nothing usable
46 + // exists yet (don't clobber a real binding installed earlier).
47 + if (typeof window.__ !== 'function') window.__ = function (text) { return text; };
48 + if (typeof window._n !== 'function') window._n = function (s, p, n) { return n === 1 ? s : p; };
49 + if (typeof window._x !== 'function') window._x = function (text) { return text; };
50 + if (typeof window.sprintf !== 'function') window.sprintf = function (fmt) { return fmt; };
51 + }
52 +})();
53 +
13 54 document.addEventListener('DOMContentLoaded', function() {
14 - // Initialize date pickers when DOM is ready
55 + function yatraGetFlatpickrLocale() {
56 + var l = window.yatraTripData && window.yatraTripData.flatpickrLocale;
57 + if (!l || typeof l !== 'object') {
58 + return { firstDayOfWeek: 1 };
59 + }
60 + var out = Object.assign({}, l);
61 + if (typeof out.firstDayOfWeek !== 'number' || isNaN(out.firstDayOfWeek)) {
62 + out.firstDayOfWeek = 1;
63 + }
64 + return out;
65 + }
66 +
67 + /**
68 + * Convert a PHP date() format string to its Flatpickr equivalent.
69 + *
70 + * The two format spec tables overlap heavily — d/j/D/l/m/n/M/F/Y/y
71 + * mean the same thing in both — but a few PHP tokens map to a
72 + * different Flatpickr token (e.g. PHP `s` = seconds → Flatpickr
73 + * `S`; PHP `a`/`A` AM-PM → Flatpickr `K`). Anything outside the
74 + * map (literal punctuation, slashes, spaces) is passed through.
75 + *
76 + * Used to populate flatpickr's `altFormat`. We keep `dateFormat`
77 + * itself locked to `Y-m-d` so the value submitted to the backend
78 + * stays stable regardless of the operator's display preference;
79 + * only the user-visible input swaps to their configured format.
80 + *
81 + * Kept in lockstep with the helper of the same shape in trip.js
82 + * and the inline mapping in single-trip-sidebar.js — three call
83 + * sites because each file ships as its own enqueued bundle and
84 + * sharing across them would require a separate dep file.
85 + *
86 + * @param {string} php PHP date() format from Settings → General → Date format
87 + * @returns {string} Flatpickr-compatible format string
88 + */
89 + function yatraPhpDateFormatToFlatpickr(php) {
90 + if (!php || typeof php !== 'string') return 'F j, Y';
91 + var map = {
92 + // Day
93 + d: 'd', j: 'j', D: 'D', l: 'l',
94 + // Month
95 + m: 'm', n: 'n', M: 'M', F: 'F',
96 + // Year
97 + Y: 'Y', y: 'y',
98 + // Time
99 + H: 'H', G: 'H', h: 'h', g: 'h',
100 + i: 'i', s: 'S', a: 'K', A: 'K'
101 + };
102 + var out = '';
103 + var esc = false;
104 + for (var i = 0; i < php.length; i++) {
105 + var ch = php[i];
106 + if (esc) { out += ch; esc = false; continue; }
107 + if (ch === '\\') { esc = true; continue; }
108 + out += map[ch] || ch;
109 + }
110 + return out || 'F j, Y';
111 + }
112 +
113 + /**
114 + * Resolve the operator's configured display format and convert it
115 + * for Flatpickr. Falls back to `F j, Y` (e.g. "Jan 5, 2026") when
116 + * settings haven't shipped a value.
117 + */
118 + function yatraResolveAltFormat() {
119 + var cfg = window.yatraTripData || {};
120 + // Pre-converted value wins (set server-side in some bundles); a
121 + // raw PHP string is converted on the fly.
122 + if (cfg.flatpickrAltFormat || cfg.altDateFormat) {
123 + return cfg.flatpickrAltFormat || cfg.altDateFormat;
124 + }
125 + var php = cfg.dateFormat || cfg.date_format || '';
126 + return yatraPhpDateFormatToFlatpickr(php);
127 + }
128 +
129 + var fpLocale = yatraGetFlatpickrLocale();
130 + var fpAltFormat = yatraResolveAltFormat();
131 +
132 + // Initialize date pickers when DOM is ready.
133 + //
134 + // Priority order applied here MUST mirror the server-side
135 + // AvailabilityResolutionService:
136 + // 1. Specific Availability Dates (per-date rows) ← highest
137 + // 2. Recurring Rules (pattern-generated dates)
138 + // 3. Trip default flexible window (available_from / available_to) ← lowest
139 + //
140 + // Server has already merged (1) and (2) into yatraTripData.availabilityDates.
141 + // When the trip is in date-specific mode (or any availability-restricted
142 + // mode the template might use) we lock the date picker to that merged list.
143 + // Otherwise we fall back to the flexible window encoded in data-min-date /
144 + // data-max-date.
15 145 const bookingDateInput = document.getElementById('travel_date');
16 146 if (bookingDateInput) {
17 147 const form = bookingDateInput.closest('.yatra-booking-form');
18 148 const bookingMode = form ? form.getAttribute('data-booking-mode') : 'regular';
19 -
20 - if (bookingMode === 'availability') {
21 - // Availability-based: Only specific dates
22 - const availabilityDates = window.yatraTripData?.availabilityDates || [];
23 -
149 +
150 + // Accept any of the historic / current values that signal
151 + // "restrict to specific availability dates". The PHP template now
152 + // emits "date_specific"; older builds used "availability". Both
153 + // must trigger the restricted picker so the priority chain holds.
154 + const isDateSpecific = (
155 + bookingMode === 'date_specific'
156 + || bookingMode === 'availability'
157 + || bookingMode === 'date-specific'
158 + );
159 +
160 + if (isDateSpecific) {
161 + // Availability-based: only the dates resolved by the priority
162 + // chain (specific dates first, then recurring rules) are pickable.
163 + const availabilityDates = (window.yatraTripData && Array.isArray(window.yatraTripData.availabilityDates))
164 + ? window.yatraTripData.availabilityDates
165 + : [];
166 +
24 167 if (availabilityDates.length > 0) {
25 168 flatpickr(bookingDateInput, {
169 + // Backend value stays as Y-m-d (what the booking
170 + // form submits); only the user-visible input flips
171 + // to the operator's configured Date Format via
172 + // altInput/altFormat. Previously hardcoded which
173 + // ignored Settings → General → Date format.
26 174 dateFormat: 'Y-m-d',
175 + altInput: true,
176 + altFormat: fpAltFormat,
27 177 minDate: 'today',
28 178 enable: availabilityDates,
29 - disableMobile: false
179 + disableMobile: false,
180 + locale: fpLocale
30 181 });
182 + } else {
183 + // Edge case: template believes the trip is date-specific but
184 + // the merged date list is empty (e.g. all rules expired).
185 + // Disable the input rather than silently falling back to
186 + // "any date", which would violate the configured priority.
187 + bookingDateInput.setAttribute('disabled', 'disabled');
188 + // Fallback placeholder when the template hasn't supplied
189 + // `data-empty-text`. Wrapped in __() so the literal string
190 + // can be translated; the data-attribute fallback is a
191 + // server-rendered string that's already localised.
192 + bookingDateInput.setAttribute(
193 + 'placeholder',
194 + bookingDateInput.getAttribute('data-empty-text') || __('No departure dates available', 'yatra')
195 + );
31 196 }
32 197 } else {
33 - // Flexible booking: Any future date with constraints
198 + // Flexible booking: any future date inside the trip's available
199 + // window (available_from / available_to are surfaced as
200 + // data-min-date / data-max-date by the sidebar template).
34 201 const minDate = bookingDateInput.getAttribute('data-min-date') || 'today';
35 202 const maxDate = bookingDateInput.getAttribute('data-max-date');
36 -
203 +
204 + // Flexible-date picker. Same altInput/altFormat pattern
205 + // as the availability-restricted branch above — backend
206 + // value stable, display matches operator preference.
37 207 const config = {
38 208 dateFormat: 'Y-m-d',
209 + altInput: true,
210 + altFormat: fpAltFormat,
39 211 minDate: minDate,
40 - disableMobile: false
212 + disableMobile: false,
213 + locale: fpLocale
41 214 };
42 -
215 +
43 216 if (maxDate) {
44 217 config.maxDate = maxDate;
45 218 }
46 -
219 +
47 220 flatpickr(bookingDateInput, config);
48 221 }
49 222 }
50 -
51 - // Enquiry modal date picker (always flexible)
223 +
224 + // Enquiry modal date picker (always flexible). Same altFormat
225 + // treatment so the enquiry form respects the operator's date
226 + // format too — previously hardcoded to Y-m-d which jarred with
227 + // any non-ISO display preference.
52 228 const enquiryDateInput = document.getElementById('enquiry-travel-date');
53 229 if (enquiryDateInput) {
54 230 flatpickr(enquiryDateInput, {
55 231 dateFormat: 'Y-m-d',
232 + altInput: true,
233 + altFormat: fpAltFormat,
56 234 minDate: 'today',
57 - disableMobile: false
235 + disableMobile: false,
236 + locale: fpLocale
58 237 });
59 238 }
60 239 });
61 240
@@ -165,61 +344,55 @@
165 344 }
166 345
167 346 return discountAmount;
168 347 }
348 +
349 + function getSidebarAvailabilities() {
350 + const raw = window.yatraTripData && window.yatraTripData.sidebarAvailability;
351 + return Array.isArray(raw) ? raw : [];
352 + }
169 353
170 354 // Update pricing when date changes
171 355 if (dateInput) {
172 356 dateInput.addEventListener('change', function() {
173 357 const selectedDate = this.value;
174 - const bookingCard = document.querySelector('.yatra-booking-card');
175 -
176 - if (bookingCard) {
177 - const availabilityData = bookingCard.getAttribute('data-availability');
178 - if (availabilityData) {
179 - try {
180 - const availabilities = JSON.parse(availabilityData);
181 - selectedAvailability = availabilities.find(a => a.departure_date === selectedDate);
182 -
183 - // Update traveler input prices if traveler-based pricing
184 - if (selectedAvailability && selectedAvailability.pricing_type === 'traveler_based' && selectedAvailability.price_types) {
185 - selectedAvailability.price_types.forEach((pt, index) => {
186 - const input = document.querySelector(`input[name="travelers[${pt.category_id}]"]`);
187 - if (input) {
188 - const price = pt.discounted_price || pt.original_price || 0;
189 - input.setAttribute('data-price', price);
190 - }
191 - });
358 + const availabilities = getSidebarAvailabilities();
359 + if (availabilities.length > 0) {
360 + selectedAvailability = availabilities.find(a => a.departure_date === selectedDate);
361 +
362 + if (selectedAvailability && selectedAvailability.pricing_type === 'traveler_based' && selectedAvailability.price_types) {
363 + selectedAvailability.price_types.forEach((pt) => {
364 + const input = document.querySelector(`input[name="travelers[${pt.category_id}]"]`);
365 + if (input) {
366 + const price = pt.discounted_price || pt.original_price || 0;
367 + input.setAttribute('data-price', price);
192 368 }
193 -
194 - // Recalculate total
195 - updatePricing();
196 - } catch (e) {
197 - console.error('Error parsing availability data:', e);
198 - }
369 + });
199 370 }
371 + } else {
372 + selectedAvailability = null;
200 373 }
374 +
375 + updatePricing();
201 376 });
202 377
203 - // Set today's date by default if available, otherwise use trip fallback pricing
204 - const today = new Date().toISOString().split('T')[0];
205 - const bookingCard = document.querySelector('.yatra-booking-card');
206 - if (bookingCard && !dateInput.value) {
207 - const availabilityData = bookingCard.getAttribute('data-availability');
208 - if (availabilityData) {
209 - try {
210 - const availabilities = JSON.parse(availabilityData);
211 - const todayAvailability = availabilities.find(a => a.departure_date === today);
212 - if (todayAvailability) {
213 - dateInput.value = today;
214 - dateInput.dispatchEvent(new Event('change'));
215 - } else {
216 - // No availability for today - use trip's default pricing (already set in HTML)
217 - // Just trigger initial calculation with trip prices
218 - updatePricing();
219 - }
220 - } catch (e) {
221 - console.error('Error setting default date:', e);
378 + // Set today's date by default if available, otherwise use trip fallback pricing.
379 + // Build LOCAL YYYY-MM-DD, not toISOString() (which is UTC and can land on
380 + // the wrong calendar day — mis-matching the availability's departure_date
381 + // and pre-selecting the wrong date near midnight).
382 + const _now = new Date();
383 + const today = _now.getFullYear() + '-'
384 + + String(_now.getMonth() + 1).padStart(2, '0') + '-'
385 + + String(_now.getDate()).padStart(2, '0');
386 + if (!dateInput.value) {
387 + const availabilities = getSidebarAvailabilities();
388 + if (availabilities.length > 0) {
389 + const todayAvailability = availabilities.find(a => a.departure_date === today);
390 + if (todayAvailability) {
391 + dateInput.value = today;
392 + dateInput.dispatchEvent(new Event('change'));
393 + } else {
394 + updatePricing();
222 395 }
223 396 }
224 397 }
225 398 }
@@ -238,13 +411,18 @@
238 411 categoryInputs.forEach(input => {
239 412 const count = parseInt(input.value) || 0;
240 413 const price = parseFloat(input.dataset.price) || 0;
241 414 const pricingMode = input.dataset.pricingMode || 'per_person';
242 -
415 + const groupOverflow = input.dataset.groupOverflow || 'block';
416 + const maxPax = parseInt(input.dataset.maxPax) || 0;
417 +
243 418 if (pricingMode === 'per_group') {
244 - // Per group: charge flat price once if any travelers
419 + // Per group: one flat price, or per block of maxPax people
420 + // when overflow is "per_block".
245 421 if (count > 0) {
246 - subtotal += price;
422 + subtotal += (groupOverflow === 'per_block' && maxPax > 0)
423 + ? price * Math.ceil(count / maxPax)
424 + : price;
247 425 }
248 426 } else {
249 427 // Per person: charge per traveler
250 428 subtotal += price * count;
@@ -292,9 +470,9 @@
292 470 <div class="yatra-group-discount-badge">
293 471 <svg width="14" height="14" fill="currentColor" viewBox="0 0 20 20">
294 472 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
295 473 </svg>
296 - Group discount applied: -${window.yatraTripData?.currencySymbol || '$'}${discountAmount.toFixed(2)}
474 + Group discount applied: -${typeof window.yatra_format_price_js === 'function' ? window.yatra_format_price_js(discountAmount) : (window.yatraTripData?.currencySymbol || '$') + discountAmount.toFixed(2)}
297 475 </div>
298 476 `;
299 477 } else if (indicator) {
300 478 indicator.remove();
@@ -335,12 +513,19 @@
335 513 // Traveler-based pricing - set adults to group size
336 514 const adultInput = document.querySelector('input[name*="travelers[adults]"]');
337 515 if (adultInput) {
338 516 adultInput.value = groupSize;
339 - // Update display
517 + // Update display. The literal "Adult x %d"
518 + // is kept as the msgid so translators can
519 + // localise both the noun and the format
520 + // (some locales put the multiplier first).
521 + // Comment phrasing matches the other call
522 + // sites in trip.js so WP-CLI sees ONE
523 + // canonical translators-comment per msgid.
340 524 const display = document.querySelector('.yatra-participants-display');
341 525 if (display) {
342 - display.textContent = `Adult x ${groupSize}`;
526 + // translators: %d: number of adults
527 + display.textContent = __('Adult x %d', 'yatra').replace('%d', String(groupSize));
343 528 }
344 529 }
345 530 }
346 531
@@ -387,10 +572,10 @@
387 572 }
388 573
389 574 if (!input) return;
390 575
391 - const min = parseInt(input.getAttribute('min')) || 0;
392 - const max = parseInt(input.getAttribute('max')) || 99;
576 + const min = window.yatraNumOr(input.getAttribute('min'), 0);
577 + const max = window.yatraNumOr(input.getAttribute('max'), 99);
393 578 let value = parseInt(input.value) || 0;
394 579
395 580 // Update value based on button type
396 581 if (btn.classList.contains('yatra-quantity-plus')) {
@@ -446,9 +631,9 @@
446 631 // Regular pricing - sync all num_travelers and enquiry_adults inputs
447 632 const regularInputs = document.querySelectorAll('#num_travelers, #enquiry_adults');
448 633 regularInputs.forEach(input => {
449 634 if (input !== sourceInput) {
450 - const max = parseInt(input.getAttribute('max')) || 99;
635 + const max = window.yatraNumOr(input.getAttribute('max'), 99);
451 636 input.value = Math.min(sourceValue, max);
452 637 }
453 638 });
454 639
@@ -455,9 +640,9 @@
455 640 // Also sync to availability section inputs
456 641 const availabilityInputs = document.querySelectorAll('input[id^="num-travelers-"]');
457 642 availabilityInputs.forEach(input => {
458 643 if (input !== sourceInput) {
459 - const max = parseInt(input.getAttribute('max')) || 99;
644 + const max = window.yatraNumOr(input.getAttribute('max'), 99);
460 645 input.value = Math.min(sourceValue, max);
461 646 }
462 647 });
463 648
@@ -482,15 +667,15 @@
482 667 );
483 668
484 669 categoryInputs.forEach(input => {
485 670 if (input !== sourceInput) {
486 - const max = parseInt(input.getAttribute('max')) || 99;
671 + const max = window.yatraNumOr(input.getAttribute('max'), 99);
487 672 input.value = Math.min(sourceValue, max);
488 673
489 674 // Update button states for this input
490 675 const row = input.closest('.yatra-quantity-row');
491 676 if (row) {
492 - const min = parseInt(input.getAttribute('min')) || 0;
677 + const min = window.yatraNumOr(input.getAttribute('min'), 0);
493 678 const minusBtn = row.querySelector('.yatra-quantity-minus');
494 679 const plusBtn = row.querySelector('.yatra-quantity-plus');
495 680 if (minusBtn) minusBtn.disabled = input.value <= min;
496 681 if (plusBtn) plusBtn.disabled = input.value >= max;
@@ -524,10 +709,15 @@
524 709
525 710 inputs.forEach(input => {
526 711 const value = parseInt(input.value) || 0;
527 712 if (value > 0) {
528 - const label = input.getAttribute('data-category-label') || 'Traveler';
529 - parts.push(`${label} x ${value}`);
713 + const label = input.getAttribute('data-category-label') || __('Traveler', 'yatra');
714 + parts.push(sprintf(
715 + /* translators: 1: traveler category label, 2: count. */
716 + __('%1$s x %2$d', 'yatra'),
717 + label,
718 + value
719 + ));
530 720 total += value;
531 721 }
532 722 });
533 723
@@ -533,9 +723,9 @@
533 723
534 724 if (parts.length > 0) {
535 725 display.textContent = parts.join(', ');
536 726 } else {
537 - display.textContent = 'Select travelers';
727 + display.textContent = __('Select travelers', 'yatra');
538 728 }
539 729 });
540 730 }
541 731 });