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/booking.js +229 -28 3.0.6 → 3.0.16 View file →
@@ -11,22 +11,45 @@
11 11 // runtime is enqueued (it is — `yatra-booking` declares `wp-i18n` as a
12 12 // dependency in FrontendAssetsProvider). Falls back to the source string if
13 13 // wp.i18n is unavailable so the page never breaks on older environments.
14 14 (function () {
15 - if (typeof window.__ === 'function') return;
16 - if (window.wp && window.wp.i18n && typeof window.wp.i18n.__ === 'function') {
17 - window.__ = function (text, domain) { return window.wp.i18n.__(text, domain || 'yatra'); };
18 - window._n = function (s, p, n, domain) { return window.wp.i18n._n(s, p, n, domain || 'yatra'); };
19 - window._x = function (text, ctx, domain) { return window.wp.i18n._x(text, ctx, domain || 'yatra'); };
20 - window.sprintf = window.sprintf || (window.wp.i18n.sprintf || function (fmt) { return fmt; });
15 + var i18n = (window.wp && window.wp.i18n) ? window.wp.i18n : null;
16 + if (i18n && typeof i18n.__ === 'function') {
17 + window.__ = function (text, domain) { return i18n.__(text, domain || 'yatra'); };
18 + window._n = function (s, p, n, domain) { return i18n._n(s, p, n, domain || 'yatra'); };
19 + window._x = function (text, ctx, domain) { return i18n._x(text, ctx, domain || 'yatra'); };
20 + // Always bind WordPress's own sprintf (it substitutes %d/%s/%1$s).
21 + // Do NOT defer to a pre-existing window.sprintf: a theme or other
22 + // plugin can define an incompatible global that leaves placeholders
23 + // unsubstituted (the cause of literal "%d"/"%s" in the UI).
24 + if (typeof i18n.sprintf === 'function') {
25 + window.sprintf = function () { return i18n.sprintf.apply(i18n, arguments); };
26 + } else if (typeof window.sprintf !== 'function') {
27 + window.sprintf = function (fmt) { return fmt; };
28 + }
21 29 } else {
22 - window.__ = function (text) { return text; };
23 - window._n = function (s, p, n) { return n === 1 ? s : p; };
24 - window._x = function (text) { return text; };
25 - window.sprintf = window.sprintf || function (fmt) { return fmt; };
30 + // wp.i18n not present — install passthroughs only if nothing usable
31 + // exists yet (don't clobber a real binding installed earlier).
32 + if (typeof window.__ !== 'function') window.__ = function (text) { return text; };
33 + if (typeof window._n !== 'function') window._n = function (s, p, n) { return n === 1 ? s : p; };
34 + if (typeof window._x !== 'function') window._x = function (text) { return text; };
35 + if (typeof window.sprintf !== 'function') window.sprintf = function (fmt) { return fmt; };
26 36 }
27 37 })();
28 38
39 +// Parse a numeric min/max attribute while preserving a legitimate 0.
40 +// `parseInt(el.getAttribute('max')) || 20` treated max="0" — what a sold-out
41 +// departure renders — as "unset" and fell back to the placeholder ceiling, so
42 +// the guest selector stayed usable and ignored the departure's real capacity.
43 +// Only an absent or non-numeric value may fall back. Installed once and shared
44 +// by every frontend script, so load order does not matter.
45 +if (typeof window.yatraNumOr !== 'function') {
46 + window.yatraNumOr = function (raw, fallback) {
47 + var parsed = parseInt(raw, 10);
48 + return isNaN(parsed) ? fallback : parsed;
49 + };
50 +}
51 +
29 52 (function($) {
30 53 'use strict';
31 54
32 55 // API configuration - available throughout the module
@@ -54,8 +77,43 @@
54 77
55 78 $(document).ready(function() {
56 79 const $form = $('#yatra-booking-form');
57 80 const $submitBtn = $('#yatra-submit-booking');
81 +
82 + // --- Guest "Create an account" reveal ---
83 + // The optional account-creation block (#yatra-account-fields) ships hidden
84 + // with non-required password inputs. Without this handler the guest can
85 + // tick "Create an account" but never see a password field, so either no
86 + // account is created (empty password) or autofill populates the hidden
87 + // field asymmetrically and the server rejects with "Passwords do not
88 + // match" — with no visible field to correct. Reveal + require the fields
89 + // only when opted in; hide + clear + un-require otherwise so a pure-guest
90 + // submit is never blocked and no stale/autofilled password is sent.
91 + (function initCreateAccountToggle() {
92 + const $accountFields = $('#yatra-account-fields');
93 + const $checkbox = $('#create-account');
94 + if (!$accountFields.length || !$checkbox.length) {
95 + return;
96 + }
97 + const $pw = $accountFields.find('#account_password');
98 + const $pwConfirm = $accountFields.find('#account_password_confirm');
99 +
100 + const applyState = function (checked) {
101 + if (checked) {
102 + $accountFields.show();
103 + $pw.add($pwConfirm).attr('required', 'required');
104 + } else {
105 + $accountFields.hide();
106 + $pw.add($pwConfirm).removeAttr('required').val('');
107 + }
108 + };
109 +
110 + // Honor the current state on load (covers browser-restored checkboxes).
111 + applyState($checkbox.is(':checked'));
112 + $checkbox.on('change', function () {
113 + applyState($(this).is(':checked'));
114 + });
115 + })();
58 116 const isRemainingPayment = Boolean(window.yatraBookingData?.isRemainingPayment) || ($form.data('is-remaining-payment') === 'yes');
59 117 const remainingAmount = isRemainingPayment
60 118 ? parseFloat(window.yatraBookingData?.remainingAmount ?? $form.data('payment-due')) || 0
61 119 : 0;
@@ -153,14 +211,20 @@
153 211 let total = 0;
154 212 $('.yatra-qty-input[data-category-id]').each(function() {
155 213 const count = parseInt($(this).val()) || 0;
156 214 const price = parseFloat($(this).data('price')) || 0;
157 - const pricingMode = $(this).data('pricing-mode') || $(this).closest('.yatra-quantity-row').data('pricing-mode') || 'per_person';
158 -
215 + const $row = $(this).closest('.yatra-quantity-row');
216 + const pricingMode = $(this).data('pricing-mode') || $row.data('pricing-mode') || 'per_person';
217 + const groupOverflow = $(this).data('group-overflow') || $row.data('group-overflow') || 'block';
218 + const maxPax = parseInt($(this).data('max-pax') || $row.data('max-pax')) || 0;
219 +
159 220 if (pricingMode === 'per_group') {
160 - // Per group: charge flat price once if any travelers in this category
221 + // Per group: one flat price for the whole group, or one flat
222 + // price per block of maxPax people when overflow is "per_block".
161 223 if (count > 0) {
162 - total += price;
224 + total += (groupOverflow === 'per_block' && maxPax > 0)
225 + ? price * Math.ceil(count / maxPax)
226 + : price;
163 227 }
164 228 } else {
165 229 // Per person: charge per traveler
166 230 total += count * price;
@@ -248,13 +312,31 @@
248 312 .split('.').join('__DECIMAL__')
249 313 .replace(/__THOUSAND__/g, thousandSeparator)
250 314 .replace(/__DECIMAL__/g, decimalSeparator);
251 315
252 - // Position currency based on settings (before or after)
253 - if (currencyPosition === 'right' || currencyPosition === 'after') {
316 + // Placement must match PHP yatra_format_price() exactly: the Settings
317 + // UI uses left / right / left_space / right_space; legacy values are
318 + // before (= left_space) and after (= right_space). Only *_space adds a
319 + // space, so a "left" setting renders "$208.35", not "$ 208.35".
320 + let pos = String(currencyPosition || '').toLowerCase().trim();
321 + if (pos === 'before') {
322 + pos = 'left_space';
323 + } else if (pos === 'after') {
324 + pos = 'right_space';
325 + }
326 + if (['left', 'right', 'left_space', 'right_space'].indexOf(pos) === -1) {
327 + pos = 'left_space';
328 + }
329 +
330 + if (pos === 'right') {
331 + return formattedAmount + symbol;
332 + }
333 + if (pos === 'right_space') {
254 334 return formattedAmount + ' ' + symbol;
255 335 }
256 -
336 + if (pos === 'left') {
337 + return symbol + formattedAmount;
338 + }
257 339 return symbol + ' ' + formattedAmount;
258 340 }
259 341
260 342 /**
@@ -331,9 +413,9 @@
331 413 }
332 414 const flexDue = paymentMethod === 'deposit' || paymentMethod === 'partial';
333 415
334 416 if (isOffline) {
335 - $buttonText.text(__('Complete Booking', 'yatra'));
417 + $buttonText.text(window.yatraBookingData?.i18n?.complete_booking || __('Complete Booking', 'yatra'));
336 418 const showAmount = flexDue ? due : totalSafe;
337 419 if (showAmount > 0) {
338 420 $payAmount.text(formatCurrency(showAmount, currency)).show();
339 421 } else {
@@ -339,9 +421,9 @@
339 421 } else {
340 422 $payAmount.hide();
341 423 }
342 424 } else {
343 - $buttonText.text(__('Pay Now', 'yatra'));
425 + $buttonText.text(window.yatraBookingData?.i18n?.pay_now || __('Pay Now', 'yatra'));
344 426 const payVal = flexDue && due > 0 ? due : totalSafe;
345 427 if (payVal > 0) {
346 428 $payAmount.text(formatCurrency(payVal, currency)).show();
347 429 } else {
@@ -361,9 +443,15 @@
361 443 if (!$firstTraveler.length) return;
362 444
363 445 // Clone the first traveler form
364 446 const $newTraveler = $firstTraveler.clone();
365 -
447 +
448 + // Additional travelers are never the lead, so drop any fields the
449 + // operator marked "Lead traveler only" (data-applies-to="lead").
450 + // Traveler 1 is rendered server-side with these; clones must not
451 + // carry them. ('all'/unset fields stay.)
452 + $newTraveler.find('[data-applies-to="lead"]').remove();
453 +
366 454 // Update index references
367 455 $newTraveler.attr('data-traveler-index', index);
368 456 $newTraveler.find('.yatra-traveler-title').text(
369 457 /* translators: %d: traveler sequence number. */
@@ -492,11 +580,22 @@
492 580
493 581 // Validate all required fields
494 582 $form.find('input[required], select[required], textarea[required]').each(function() {
495 583 const $field = $(this);
496 - const value = $field.val();
497 -
498 - if (!value || value.trim() === '') {
584 + const type = ($field.attr('type') || '').toLowerCase();
585 +
586 + // Checkbox/radio: .val() returns the value attribute regardless of
587 + // checked state, so an unchecked required box would wrongly pass.
588 + // Test the checked state instead.
589 + let invalid;
590 + if (type === 'checkbox' || type === 'radio') {
591 + invalid = !$field.is(':checked');
592 + } else {
593 + const value = $field.val();
594 + invalid = !value || value.trim() === '';
595 + }
596 +
597 + if (invalid) {
499 598 isValid = false;
500 599 $field.addClass('error');
501 600 const label = $field.closest('.yatra-form-group').find('label').text().replace('*', '').trim();
502 601 errors.push(sprintf(
@@ -584,9 +683,14 @@
584 683 '<p style="font-size: 18px; color: #6b7280; margin-bottom: 8px;">' + message + '</p>' +
585 684 '<p style="font-size: 16px; color: #111827; font-weight: 600;">' + referenceText + '</p>' +
586 685 footerHtml +
587 686 '</div>');
588 -
687 +
688 + // The booking-form header ("Complete Your Booking" + subtitle) is a
689 + // SIBLING of the form, not inside it — replacing the form alone left
690 + // that header showing above the confirmation. Hide it so the success
691 + // screen doesn't repeat the booking-form heading.
692 + $form.siblings('.yatra-booking-header').hide();
589 693 $form.html($success);
590 694 }
591 695
592 696 // =====================
@@ -626,10 +730,10 @@
626 730
627 731 const $btn = $(this);
628 732 const $input = $('#number-of-travelers');
629 733 let currentValue = parseInt($input.val()) || 1;
630 - const min = parseInt($input.attr('min')) || 1;
631 - const max = parseInt($input.attr('max')) || 20;
734 + const min = window.yatraNumOr($input.attr('min'), 1);
735 + const max = window.yatraNumOr($input.attr('max'), 20);
632 736
633 737 if ($btn.hasClass('yatra-qty-plus')) {
634 738 if (currentValue < max) {
635 739 currentValue++;
@@ -659,10 +763,10 @@
659 763 const $btn = $(this);
660 764 const $row = $btn.closest('.yatra-quantity-row');
661 765 const $input = $row.find('.yatra-qty-input');
662 766 let currentValue = parseInt($input.val()) || 0;
663 - const min = parseInt($input.attr('min')) || 0;
664 - const max = parseInt($input.attr('max')) || 20;
767 + const min = window.yatraNumOr($input.attr('min'), 0);
768 + const max = window.yatraNumOr($input.attr('max'), 20);
665 769
666 770 if ($btn.hasClass('yatra-qty-plus')) {
667 771 if (currentValue < max) {
668 772 currentValue++;
@@ -1048,8 +1152,25 @@
1048 1152 /**
1049 1153 * Submit booking to server
1050 1154 */
1051 1155 function submitBookingToServer(bookingData, originalBtnHtml) {
1156 + // reCAPTCHA v3: attach a token when the booking form is protected,
1157 + // then run the real submit. Wraps every call path (direct submit and
1158 + // the gateway `proceedWithSubmission` hook). No-op unless the operator
1159 + // opted booking into reCAPTCHA (off by default), so payment flows are
1160 + // unchanged otherwise.
1161 + const yatraRc = window.yatraRecaptcha;
1162 + if (yatraRc && yatraRc.protects && yatraRc.protects('booking') && !bookingData.recaptcha_token) {
1163 + yatraRc.execute('booking').then(function (recaptchaToken) {
1164 + if (recaptchaToken) { bookingData.recaptcha_token = recaptchaToken; }
1165 + submitBookingToServerInner(bookingData, originalBtnHtml);
1166 + });
1167 + return;
1168 + }
1169 + submitBookingToServerInner(bookingData, originalBtnHtml);
1170 + }
1171 +
1172 + function submitBookingToServerInner(bookingData, originalBtnHtml) {
1052 1173 const { base: restBase, isPlain } = getRestBase();
1053 1174 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
1054 1175 let createUrl;
1055 1176 if (isPlain) {
@@ -1786,8 +1907,24 @@
1786 1907 password: password,
1787 1908 confirm_password: confirmPassword
1788 1909 };
1789 1910
1911 + // reCAPTCHA v3: this form posts straight to /auth/register, so it must
1912 + // carry its own token when the operator protects registration — the
1913 + // server rejects an empty one outright, whatever the score threshold.
1914 + // Resolves to '' when reCAPTCHA is off or unavailable, so the normal
1915 + // (unprotected) flow is completely unchanged.
1916 + const yatraRcReg = window.yatraRecaptcha;
1917 + const regTokenPromise = (yatraRcReg && typeof yatraRcReg.protects === 'function'
1918 + && yatraRcReg.protects('registration') && typeof yatraRcReg.execute === 'function')
1919 + ? yatraRcReg.execute('registration')
1920 + : Promise.resolve('');
1921 +
1922 + regTokenPromise.catch(function () { return ''; }).then(function (regToken) {
1923 + if (regToken) {
1924 + registerData.recaptcha_token = regToken;
1925 + }
1926 +
1790 1927 fetch(apiUrl + '/auth/register', {
1791 1928 method: 'POST',
1792 1929 headers: {
1793 1930 'Content-Type': 'application/json',
@@ -1851,8 +1988,72 @@
1851 1988 $btn.prop('disabled', false);
1852 1989 $btnText.show();
1853 1990 $btnLoading.hide();
1854 1991 });
1992 + }); // end reCAPTCHA token wrapper
1855 1993 });
1994 +
1995 + // ---------------------------------------------------------------------
1996 + // Date field picker upgrade (fast year navigation for Date of Birth)
1997 + // ---------------------------------------------------------------------
1998 + // A native <input type="date"> popup only steps one month at a time, so
1999 + // reaching a birth year like 1990 takes many clicks. Where flatpickr is
2000 + // available (and on non-touch devices, since mobile native pickers already
2001 + // have good year wheels) we upgrade `.yatra-js-date` inputs to a flatpickr
2002 + // whose header year is directly typeable/scrollable. Fully guarded: with no
2003 + // flatpickr, or on mobile, the native input is left untouched and working.
2004 + var yatraIsMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent || '');
2005 +
2006 + function yatraUpgradeDateField(input) {
2007 + if (!input || input.dataset.yatraFp === '1') return;
2008 + if (typeof window.flatpickr === 'undefined') return;
2009 + if (yatraIsMobile) return; // native mobile date wheels already navigate years well
2010 + input.dataset.yatraFp = '1';
2011 + var isDob = input.getAttribute('data-yatra-dob') === '1';
2012 + // Turn off the native date UI so flatpickr is the only picker; the
2013 + // submitted value stays ISO (Y-m-d) so FormData/back-end are unchanged.
2014 + try { input.type = 'text'; } catch (e) {}
2015 + window.flatpickr(input, {
2016 + dateFormat: 'Y-m-d',
2017 + allowInput: true,
2018 + maxDate: isDob ? 'today' : null,
2019 + defaultDate: input.value || null
2020 + });
2021 + }
2022 +
2023 + function yatraUpgradeDateFields(root) {
2024 + var scope = (root && root.querySelectorAll) ? root : document;
2025 + var list = scope.querySelectorAll('input.yatra-js-date');
2026 + for (var i = 0; i < list.length; i++) {
2027 + yatraUpgradeDateField(list[i]);
2028 + }
2029 + }
2030 +
2031 + function yatraInitDatePickers() {
2032 + yatraUpgradeDateFields(document);
2033 + // The traveler form is injected via AJAX, so watch for inserted inputs.
2034 + if (typeof MutationObserver === 'undefined' || !document.body) return;
2035 + var obs = new MutationObserver(function (mutations) {
2036 + for (var m = 0; m < mutations.length; m++) {
2037 + var added = mutations[m].addedNodes;
2038 + for (var n = 0; n < added.length; n++) {
2039 + var node = added[n];
2040 + if (!node || node.nodeType !== 1) continue;
2041 + if (node.matches && node.matches('input.yatra-js-date')) {
2042 + yatraUpgradeDateField(node);
2043 + } else if (node.querySelectorAll) {
2044 + yatraUpgradeDateFields(node);
2045 + }
2046 + }
2047 + }
2048 + });
2049 + obs.observe(document.body, { childList: true, subtree: true });
2050 + }
2051 +
2052 + if (document.readyState === 'loading') {
2053 + document.addEventListener('DOMContentLoaded', yatraInitDatePickers);
2054 + } else {
2055 + yatraInitDatePickers();
2056 + }
1856 2057
1857 2058 // End of document ready
1858 2059 })(jQuery);