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 +234 -32 3.0.3 → 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
@@ -195,10 +374,16 @@
195 374
196 375 updatePricing();
197 376 });
198 377
199 - // Set today's date by default if available, otherwise use trip fallback pricing
200 - const today = new Date().toISOString().split('T')[0];
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');
201 386 if (!dateInput.value) {
202 387 const availabilities = getSidebarAvailabilities();
203 388 if (availabilities.length > 0) {
204 389 const todayAvailability = availabilities.find(a => a.departure_date === today);
@@ -226,13 +411,18 @@
226 411 categoryInputs.forEach(input => {
227 412 const count = parseInt(input.value) || 0;
228 413 const price = parseFloat(input.dataset.price) || 0;
229 414 const pricingMode = input.dataset.pricingMode || 'per_person';
230 -
415 + const groupOverflow = input.dataset.groupOverflow || 'block';
416 + const maxPax = parseInt(input.dataset.maxPax) || 0;
417 +
231 418 if (pricingMode === 'per_group') {
232 - // 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".
233 421 if (count > 0) {
234 - subtotal += price;
422 + subtotal += (groupOverflow === 'per_block' && maxPax > 0)
423 + ? price * Math.ceil(count / maxPax)
424 + : price;
235 425 }
236 426 } else {
237 427 // Per person: charge per traveler
238 428 subtotal += price * count;
@@ -280,9 +470,9 @@
280 470 <div class="yatra-group-discount-badge">
281 471 <svg width="14" height="14" fill="currentColor" viewBox="0 0 20 20">
282 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"/>
283 473 </svg>
284 - 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)}
285 475 </div>
286 476 `;
287 477 } else if (indicator) {
288 478 indicator.remove();
@@ -323,12 +513,19 @@
323 513 // Traveler-based pricing - set adults to group size
324 514 const adultInput = document.querySelector('input[name*="travelers[adults]"]');
325 515 if (adultInput) {
326 516 adultInput.value = groupSize;
327 - // 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.
328 524 const display = document.querySelector('.yatra-participants-display');
329 525 if (display) {
330 - display.textContent = `Adult x ${groupSize}`;
526 + // translators: %d: number of adults
527 + display.textContent = __('Adult x %d', 'yatra').replace('%d', String(groupSize));
331 528 }
332 529 }
333 530 }
334 531
@@ -375,10 +572,10 @@
375 572 }
376 573
377 574 if (!input) return;
378 575
379 - const min = parseInt(input.getAttribute('min')) || 0;
380 - 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);
381 578 let value = parseInt(input.value) || 0;
382 579
383 580 // Update value based on button type
384 581 if (btn.classList.contains('yatra-quantity-plus')) {
@@ -434,9 +631,9 @@
434 631 // Regular pricing - sync all num_travelers and enquiry_adults inputs
435 632 const regularInputs = document.querySelectorAll('#num_travelers, #enquiry_adults');
436 633 regularInputs.forEach(input => {
437 634 if (input !== sourceInput) {
438 - const max = parseInt(input.getAttribute('max')) || 99;
635 + const max = window.yatraNumOr(input.getAttribute('max'), 99);
439 636 input.value = Math.min(sourceValue, max);
440 637 }
441 638 });
442 639
@@ -443,9 +640,9 @@
443 640 // Also sync to availability section inputs
444 641 const availabilityInputs = document.querySelectorAll('input[id^="num-travelers-"]');
445 642 availabilityInputs.forEach(input => {
446 643 if (input !== sourceInput) {
447 - const max = parseInt(input.getAttribute('max')) || 99;
644 + const max = window.yatraNumOr(input.getAttribute('max'), 99);
448 645 input.value = Math.min(sourceValue, max);
449 646 }
450 647 });
451 648
@@ -470,15 +667,15 @@
470 667 );
471 668
472 669 categoryInputs.forEach(input => {
473 670 if (input !== sourceInput) {
474 - const max = parseInt(input.getAttribute('max')) || 99;
671 + const max = window.yatraNumOr(input.getAttribute('max'), 99);
475 672 input.value = Math.min(sourceValue, max);
476 673
477 674 // Update button states for this input
478 675 const row = input.closest('.yatra-quantity-row');
479 676 if (row) {
480 - const min = parseInt(input.getAttribute('min')) || 0;
677 + const min = window.yatraNumOr(input.getAttribute('min'), 0);
481 678 const minusBtn = row.querySelector('.yatra-quantity-minus');
482 679 const plusBtn = row.querySelector('.yatra-quantity-plus');
483 680 if (minusBtn) minusBtn.disabled = input.value <= min;
484 681 if (plusBtn) plusBtn.disabled = input.value >= max;
@@ -512,10 +709,15 @@
512 709
513 710 inputs.forEach(input => {
514 711 const value = parseInt(input.value) || 0;
515 712 if (value > 0) {
516 - const label = input.getAttribute('data-category-label') || 'Traveler';
517 - 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 + ));
518 720 total += value;
519 721 }
520 722 });
521 723
@@ -521,9 +723,9 @@
521 723
522 724 if (parts.length > 0) {
523 725 display.textContent = parts.join(', ');
524 726 } else {
525 - display.textContent = 'Select travelers';
727 + display.textContent = __('Select travelers', 'yatra');
526 728 }
527 729 });
528 730 }
529 731 });