PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.11
Yatra – Travel Booking & Tour Operator Software v3.0.11
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
yatra / assets / js / login-shortcode.js

login-shortcode.js in Yatra – Travel Booking & Tour Operator Software 3.0.11, at assets/js/login-shortcode.js

712 lines 27.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Login Shortcode JavaScript
3 *
4 * Production-optimized login form interactions and functionality
5 *
6 * @package Yatra
7 * @version 1.0.0
8 */
9
10 // Translation helpers — idempotent shim matching the other frontend bundles.
11 (function () {
12 if (typeof window.__ === 'function') return;
13 if (window.wp && window.wp.i18n && typeof window.wp.i18n.__ === 'function') {
14 window.__ = function (text, domain) { return window.wp.i18n.__(text, domain || 'yatra'); };
15 window._n = function (s, p, n, domain) { return window.wp.i18n._n(s, p, n, domain || 'yatra'); };
16 window._x = function (text, ctx, domain) { return window.wp.i18n._x(text, ctx, domain || 'yatra'); };
17 window.sprintf = window.sprintf || (window.wp.i18n.sprintf || function (fmt) { return fmt; });
18 } else {
19 window.__ = function (text) { return text; };
20 window._n = function (s, p, n) { return n === 1 ? s : p; };
21 window._x = function (text) { return text; };
22 window.sprintf = window.sprintf || function (fmt) { return fmt; };
23 }
24 })();
25
26 (function($) {
27 'use strict';
28
29 // Performance: Cache DOM elements
30 let $document = $(document);
31 let $window = $(window);
32
33 // Performance: Debounce function for performance
34 function debounce(func, wait) {
35 let timeout;
36 return function executedFunction(...args) {
37 const later = () => {
38 clearTimeout(timeout);
39 func(...args);
40 };
41 clearTimeout(timeout);
42 timeout = setTimeout(later, wait);
43 };
44 }
45
46 // Performance: Throttle function for scroll events
47 function throttle(func, limit) {
48 let inThrottle;
49 return function() {
50 const args = arguments;
51 const context = this;
52 if (!inThrottle) {
53 func.apply(context, args);
54 inThrottle = true;
55 setTimeout(() => inThrottle = false, limit);
56 }
57 };
58 }
59
60 // Initialize when DOM is ready
61 $document.ready(function() {
62 // Performance: Check if login form exists before initializing
63 if ($('.yatra-login-form').length === 0) {
64 return;
65 }
66
67 // Initialize components
68 initPasswordToggles();
69 initAjaxLogin();
70 initPanelSwitching();
71 initAjaxRegister();
72 initAjaxForgotPassword();
73 initAccessibility();
74 initPerformanceOptimizations();
75 });
76
77 /**
78 * Switch between the login / register / forgot-password panels.
79 * The links carry a real href (wp-admin) as a no-JS fallback; here we
80 * intercept and reveal the matching inline panel instead.
81 */
82 function initPanelSwitching() {
83 // Reveal `target` panel, hide the rest, and sync tab state + roving
84 // tabindex. Forgot-password is a sub-view of Login, so the Login tab
85 // stays active while it is shown.
86 function activatePanel($container, target, focusPanelInput) {
87 const $panels = $container.find('.yatra-auth-panel');
88 if (!$panels.length) {
89 return;
90 }
91 $panels.hide().attr('aria-hidden', 'true');
92 const $active = $container.find('.yatra-auth-panel[data-panel="' + target + '"]');
93 $active.show().attr('aria-hidden', 'false');
94
95 const activeTab = target === 'forgot' ? 'login' : target;
96 $container.find('.yatra-login-tab').each(function() {
97 const isActive = $(this).data('target') === activeTab;
98 $(this)
99 .toggleClass('is-active', isActive)
100 .attr('aria-selected', isActive ? 'true' : 'false')
101 .attr('tabindex', isActive ? '0' : '-1');
102 });
103
104 // Keep the shared header + footer copy in sync with the active panel.
105 // Each panel carries its own data-header-*/data-footer-text strings
106 // (server-translated), so this is a single source of truth.
107 const $root = $container.closest('.yatra-login-container');
108 const headerTitle = $active.attr('data-header-title');
109 const headerSubtitle = $active.attr('data-header-subtitle');
110 const footerText = $active.attr('data-footer-text');
111 if (typeof headerTitle === 'string') {
112 $root.find('.yatra-login-header .yatra-login-title').text(headerTitle);
113 }
114 if (typeof headerSubtitle === 'string') {
115 const $subtitle = $root.find('.yatra-login-header .yatra-login-subtitle');
116 $subtitle.text(headerSubtitle);
117 // Hide the element entirely when a panel has no subtitle so it
118 // doesn't leave an empty gap.
119 $subtitle.css('display', headerSubtitle === '' ? 'none' : '');
120 }
121 if (typeof footerText === 'string') {
122 $root.find('.yatra-login-footer .yatra-security-text').text(footerText);
123 }
124
125 // Clear any stale messages when switching panels.
126 $container.find('.yatra-form-error, .yatra-form-success').remove();
127
128 if (focusPanelInput) {
129 $active.find('input:visible').first().trigger('focus');
130 }
131 }
132
133 // Both the top tabs (.yatra-login-tab) and the in-panel text links
134 // (.yatra-auth-switch, e.g. "Forgot your password?", "Back to login")
135 // switch panels via a data-target.
136 $document.on('click', '.yatra-login-tab, .yatra-auth-switch', function(e) {
137 e.preventDefault();
138 const target = $(this).data('target');
139 if (!target) {
140 return;
141 }
142 activatePanel($(this).closest('.yatra-login-form-container'), target, true);
143 });
144
145 // WAI-ARIA tabs keyboard support: Left/Right/Home/End move between tabs
146 // (automatic activation — switching panels is cheap here).
147 $document.on('keydown', '.yatra-login-tab', function(e) {
148 if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].indexOf(e.key) === -1) {
149 return;
150 }
151 e.preventDefault();
152 const $tabs = $(this).closest('.yatra-login-tabs').find('.yatra-login-tab');
153 const count = $tabs.length;
154 if (!count) {
155 return;
156 }
157 let idx = $tabs.index(this);
158 if (e.key === 'ArrowLeft') { idx = (idx - 1 + count) % count; }
159 else if (e.key === 'ArrowRight') { idx = (idx + 1) % count; }
160 else if (e.key === 'Home') { idx = 0; }
161 else if (e.key === 'End') { idx = count - 1; }
162 const $next = $tabs.eq(idx);
163 activatePanel($next.closest('.yatra-login-form-container'), $next.data('target'), false);
164 $next.trigger('focus');
165 });
166 }
167
168 /**
169 * Validate the registration form client-side before submitting.
170 */
171 function validateRegisterForm($form) {
172 const email = $form.find('[name="email"]').val().trim();
173 const password = $form.find('[name="password"]').val();
174 const confirm = $form.find('[name="confirm_password"]').val();
175 const firstName = $form.find('[name="first_name"]').val().trim();
176 const lastName = $form.find('[name="last_name"]').val().trim();
177
178 if (!firstName || !lastName) {
179 return { valid: false, message: __('Please enter your first and last name.', 'yatra') };
180 }
181 if (!email) {
182 return { valid: false, message: __('Please enter your email address.', 'yatra') };
183 }
184 if (!password || password.length < 8) {
185 return { valid: false, message: __('Password must be at least 8 characters long.', 'yatra') };
186 }
187 if (password !== confirm) {
188 return { valid: false, message: __('Passwords do not match.', 'yatra') };
189 }
190 return { valid: true };
191 }
192
193 /**
194 * Initialize AJAX registration against the public /auth/register REST route.
195 * Reuses the same backend the booking checkout uses (email verification
196 * included), so no new server logic is introduced here.
197 */
198 function initAjaxRegister() {
199 $document.on('submit', '.yatra-register-form[data-ajax="true"]', function(e) {
200 e.preventDefault();
201 e.stopPropagation();
202
203 const $form = $(this);
204 const $submitBtn = $form.find('button[type="submit"]');
205 if ($submitBtn.prop('disabled')) {
206 return false;
207 }
208
209 const validation = validateRegisterForm($form);
210 if (!validation.valid) {
211 showFormError($form, validation.message);
212 return false;
213 }
214
215 $form.find('.yatra-form-error, .yatra-form-success').remove();
216 setButtonLoading($submitBtn, true);
217
218 const restBase = (yatra_ajax && yatra_ajax.rest_url ? yatra_ajax.rest_url : '/wp-json/yatra/v1').replace(/\/$/, '');
219 const payload = {
220 first_name: $form.find('[name="first_name"]').val().trim(),
221 last_name: $form.find('[name="last_name"]').val().trim(),
222 email: $form.find('[name="email"]').val().trim(),
223 phone: $form.find('[name="phone"]').val().trim(),
224 password: $form.find('[name="password"]').val(),
225 confirm_password: $form.find('[name="confirm_password"]').val()
226 };
227
228 // reCAPTCHA v3: attach a fresh token when registration is protected.
229 const yatraRc = window.yatraRecaptcha;
230 const regTokenPromise = (yatraRc && yatraRc.protects && yatraRc.protects('registration'))
231 ? yatraRc.execute('registration')
232 : Promise.resolve('');
233
234 regTokenPromise.then(function (recaptchaToken) {
235 if (recaptchaToken) {
236 payload.recaptcha_token = recaptchaToken;
237 }
238
239 const request = $.ajax({
240 url: restBase + '/auth/register',
241 method: 'POST',
242 contentType: 'application/json',
243 data: JSON.stringify(payload),
244 headers: yatra_ajax && yatra_ajax.rest_nonce ? { 'X-WP-Nonce': yatra_ajax.rest_nonce } : {},
245 timeout: 20000,
246 dataType: 'json'
247 });
248
249 request.done(function(response) {
250 if (response && response.success) {
251 showFormSuccess($form, response.message || __('Registration successful! Please check your email to verify your account.', 'yatra'));
252 $form[0].reset();
253 } else {
254 showFormError($form, (response && response.message) || __('Registration failed. Please try again.', 'yatra'));
255 }
256 });
257
258 request.fail(function(xhr, status) {
259 let message = __('Network error. Please check your connection and try again.', 'yatra');
260 if (status === 'timeout') {
261 message = __('Request timed out. Please try again.', 'yatra');
262 } else if (xhr.responseJSON && xhr.responseJSON.message) {
263 message = xhr.responseJSON.message;
264 }
265 showFormError($form, message);
266 });
267
268 request.always(function() {
269 setButtonLoading($submitBtn, false);
270 });
271 });
272
273 return false;
274 });
275 }
276
277 /**
278 * Initialize AJAX forgot-password. Delegates to WordPress core's secure
279 * password-reset flow server-side (retrieve_password) — we never reimplement
280 * reset-token handling. The entry point stays on the branded page.
281 */
282 function initAjaxForgotPassword() {
283 $document.on('submit', '.yatra-forgot-form[data-ajax="true"]', function(e) {
284 e.preventDefault();
285 e.stopPropagation();
286
287 const $form = $(this);
288 const $submitBtn = $form.find('button[type="submit"]');
289 if ($submitBtn.prop('disabled')) {
290 return false;
291 }
292
293 const userLogin = $form.find('[name="user_login"]').val().trim();
294 if (!userLogin) {
295 showFormError($form, __('Please enter your email address or username.', 'yatra'));
296 return false;
297 }
298
299 $form.find('.yatra-form-error, .yatra-form-success').remove();
300 setButtonLoading($submitBtn, true);
301
302 const request = $.ajax({
303 url: yatra_ajax && yatra_ajax.ajax_url ? yatra_ajax.ajax_url : '/wp-admin/admin-ajax.php',
304 method: 'POST',
305 data: {
306 action: 'yatra_ajax_lost_password',
307 user_login: userLogin,
308 yatra_login_nonce: $form.find('[name="yatra_login_nonce"]').val()
309 },
310 timeout: 20000,
311 dataType: 'json'
312 });
313
314 request.done(function(response) {
315 if (response && response.success) {
316 showFormSuccess($form, (response.data && response.data.message) || __('If an account exists for that email, a password reset link has been sent.', 'yatra'));
317 $form[0].reset();
318 } else {
319 showFormError($form, (response && response.data && response.data.message) || __('Unable to process the request. Please try again.', 'yatra'));
320 }
321 });
322
323 request.fail(function(xhr, status) {
324 let message = __('Network error. Please check your connection and try again.', 'yatra');
325 if (status === 'timeout') {
326 message = __('Request timed out. Please try again.', 'yatra');
327 } else if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
328 message = xhr.responseJSON.data.message;
329 }
330 showFormError($form, message);
331 });
332
333 request.always(function() {
334 setButtonLoading($submitBtn, false);
335 });
336
337 return false;
338 });
339 }
340
341 /**
342 * Toggle a submit button's loading state, mirroring the login button pattern.
343 */
344 function setButtonLoading($submitBtn, loading) {
345 if (loading) {
346 $submitBtn.prop('disabled', true).addClass('yatra-is-loading');
347 $submitBtn.find('.yatra-btn-text').hide();
348 $submitBtn.find('.yatra-btn-loading').show();
349 } else {
350 $submitBtn.prop('disabled', false).removeClass('yatra-is-loading');
351 $submitBtn.find('.yatra-btn-loading').hide();
352 $submitBtn.find('.yatra-btn-text').show();
353 }
354 }
355
356 /**
357 * Initialize password toggle functionality with performance optimizations
358 */
359 function initPasswordToggles() {
360 const passwordToggles = $('.yatra-password-toggle');
361
362 // Performance: Use event delegation for better performance
363 $document.on('click', '.yatra-password-toggle', function(e) {
364 e.preventDefault();
365
366 const $toggle = $(this);
367 const targetId = $toggle.data('target');
368 const $input = targetId ? $('#' + targetId) : $toggle.siblings('.yatra-form-input');
369
370 if (!$input.length) {
371 console.warn('Password input not found for toggle');
372 return;
373 }
374
375 const $eyeIcon = $toggle.find('.yatra-eye-icon');
376 const $eyeOffIcon = $toggle.find('.yatra-eye-off-icon');
377
378 try {
379 const isPassword = $input.attr('type') === 'password';
380
381 if (isPassword) {
382 $input.attr('type', 'text');
383 $eyeIcon.hide();
384 $eyeOffIcon.show();
385 $toggle.attr('aria-label', __('Hide password', 'yatra'));
386 } else {
387 $input.attr('type', 'password');
388 $eyeIcon.show();
389 $eyeOffIcon.hide();
390 $toggle.attr('aria-label', __('Show password', 'yatra'));
391 }
392 } catch (error) {
393 console.error('Error toggling password visibility:', error);
394 }
395 });
396 }
397
398 /**
399 * Enhanced form validation function
400 */
401 function validateLoginForm($username, $password) {
402 const username = $username.val().trim();
403 const password = $password.val().trim();
404
405 if (!username) {
406 return {
407 valid: false,
408 message: yatra_ajax?.strings?.validation_error || __('Please enter your email or username.', 'yatra')
409 };
410 }
411
412 if (!password) {
413 return {
414 valid: false,
415 message: yatra_ajax?.strings?.validation_error || __('Please enter your password.', 'yatra')
416 };
417 }
418
419 if (username.length > 60) {
420 return {
421 valid: false,
422 message: __('Username is too long.', 'yatra')
423 };
424 }
425
426 if (password.length > 72) {
427 return {
428 valid: false,
429 message: __('Password is too long.', 'yatra')
430 };
431 }
432
433 return { valid: true };
434 }
435
436 /**
437 * Initialize accessibility features
438 */
439 function initAccessibility() {
440 // Add ARIA labels and improve keyboard navigation
441 $document.on('keydown', '.yatra-form-input', function(e) {
442 if (e.key === 'Enter') {
443 const $form = $(this).closest('form');
444 if ($form.length) {
445 $form.trigger('submit');
446 }
447 }
448 });
449
450 // Focus management for error messages
451 $document.on('click', '.yatra-form-error', function() {
452 $(this).fadeOut(200, function() {
453 $(this).remove();
454 });
455 });
456 }
457
458 /**
459 * Initialize performance optimizations
460 */
461 function initPerformanceOptimizations() {
462 // Lazy load error styles
463 if (!$('#yatra-login-error-styles').length) {
464 $('<style id="yatra-login-error-styles">')
465 .text('.yatra-form-error { transition: opacity 0.2s ease; }')
466 .appendTo('head');
467 }
468
469 // Preload critical resources
470 if (window.performance && window.performance.getEntriesByType) {
471 // Monitor performance in development
472 if (yatra_ajax?.debug) {
473 setTimeout(() => {
474 const entries = performance.getEntriesByType('navigation');
475 if (entries.length > 0) {
476 window.YATRA_DEBUG && console.log('Login form load time:', entries[0].loadEventEnd - entries[0].loadEventStart, 'ms');
477 }
478 }, 1000);
479 }
480 }
481 }
482
483 /**
484 * Initialize AJAX login with production optimizations
485 */
486 function initAjaxLogin() {
487 const $loginForm = $('.yatra-login-form');
488
489 // Only use AJAX if the form has data-ajax="true" attribute
490 if ($loginForm.data('ajax') === true) {
491 // Performance: Use event delegation for better performance
492 $document.on('submit', '.yatra-login-form[data-ajax="true"]', function(e) {
493 e.preventDefault();
494 e.stopPropagation();
495
496 const $form = $(this);
497 const $submitBtn = $form.find('button[type="submit"], input[type="submit"]');
498 const $username = $form.find('[name="log"], [name="username"], [name="email"]');
499 const $password = $form.find('[name="pwd"], [name="password"]');
500
501 // Prevent multiple submissions
502 if ($submitBtn.prop('disabled')) {
503 return false;
504 }
505
506 // Enhanced form validation
507 const validation = validateLoginForm($username, $password);
508 if (!validation.valid) {
509 showFormError($form, validation.message);
510 return false;
511 }
512
513 // Reset previous errors
514 $('.yatra-form-error').remove();
515 // Field-level validation is already handled by validateLoginForm()
516
517 // Show loading state immediately to prevent flicker
518 $submitBtn.prop('disabled', true);
519 // Preserve the original markup once, then toggle the built-in loading spans.
520 if (!$submitBtn.data('yatraOriginalHtml')) {
521 $submitBtn.data('yatraOriginalHtml', $submitBtn.html());
522 }
523 $submitBtn.addClass('yatra-is-loading');
524 $submitBtn.find('.yatra-btn-text').hide();
525 $submitBtn.find('.yatra-btn-loading').show();
526
527 // Prepare AJAX request data
528 const formData = {
529 action: 'yatra_ajax_login',
530 username: $username.val().trim(),
531 password: $password.val().trim(),
532 rememberme: $form.find('[name="rememberme"]').is(':checked') ? 'forever' : '',
533 yatra_login_nonce: $form.find('[name="yatra_login_nonce"]').val(),
534 redirect_to: $form.find('[name="redirect_to"]').val() || '',
535 ajax: true
536 };
537
538 // Perform AJAX request with timeout
539 const ajaxRequest = $.ajax({
540 url: yatra_ajax?.ajax_url || '/wp-admin/admin-ajax.php',
541 type: 'POST',
542 data: formData,
543 timeout: 15000, // 15 second timeout
544 dataType: 'json'
545 });
546
547 // Handle success
548 ajaxRequest.done(function(response) {
549 if (response.success) {
550 // Success: Redirect or show success message
551 if (response.data && (response.data.redirect_url || response.data.redirect)) {
552 window.location.href = response.data.redirect_url || response.data.redirect;
553 } else {
554 showFormSuccess($form, response.data.message || __('Login successful!', 'yatra'));
555 setTimeout(() => {
556 window.location.reload();
557 }, 1500);
558 }
559 } else {
560 // Error: Show error message
561 showFormError($form, response.data.message || __('Login failed. Please try again.', 'yatra'));
562 }
563 });
564
565 // Handle error
566 ajaxRequest.fail(function(xhr, status, error) {
567 let errorMessage = __('Network error. Please check your connection and try again.', 'yatra');
568
569 if (status === 'timeout') {
570 errorMessage = __('Request timed out. Please try again.', 'yatra');
571 } else if (xhr.responseJSON && xhr.responseJSON.data) {
572 errorMessage = xhr.responseJSON.data.message || errorMessage;
573 }
574
575 showFormError($form, errorMessage);
576 });
577
578 // Always restore button state
579 ajaxRequest.always(function() {
580 $submitBtn.prop('disabled', false);
581 $submitBtn.removeClass('yatra-is-loading');
582 $submitBtn.find('.yatra-btn-loading').hide();
583 $submitBtn.find('.yatra-btn-text').show();
584
585 // If some other code replaced the markup, restore it.
586 const originalHtml = $submitBtn.data('yatraOriginalHtml');
587 if (originalHtml && $submitBtn.find('.yatra-btn-text').length === 0) {
588 $submitBtn.html(originalHtml);
589 }
590 });
591
592 return false;
593 });
594 }
595 }
596
597 /**
598 * Show field-specific error message
599 */
600 function showFieldError($field, message) {
601 // Remove existing field error
602 $field.siblings('.yatra-form-error').fadeOut(200, function() {
603 $(this).remove();
604 });
605
606 // Create and insert error message with fade-in to prevent flicker
607 const $error = $('<div class="yatra-form-error" style="display: none;">' + message + '</div>');
608 $error.css({
609 color: '#dc2626',
610 fontSize: '0.875rem',
611 marginTop: '4px',
612 fontWeight: '500',
613 opacity: 0,
614 transition: 'opacity 0.2s ease'
615 });
616
617 $field.after($error);
618
619 // Fade in error message
620 setTimeout(function() {
621 $error.css({
622 display: 'block',
623 opacity: 1
624 });
625 }, 10);
626
627 // Remove error on focus
628 $field.on('focus', function() {
629 $(this).removeClass('error');
630 $(this).siblings('.yatra-form-error').fadeOut(200, function() {
631 $(this).remove();
632 });
633 });
634 }
635
636 /**
637 * Show form error message
638 */
639 function showFormError($form, message) {
640 // Remove existing error with fade out
641 $form.find('.yatra-form-error').fadeOut(200, function() {
642 $(this).remove();
643 });
644
645 // Create and insert error message at the top of the form with fade-in
646 const $error = $('<div class="yatra-form-error" style="display: none;">' + message + '</div>');
647 $error.css({
648 color: '#dc2626',
649 backgroundColor: '#fef2f2',
650 border: '1px solid #fecaca',
651 borderRadius: '8px',
652 padding: '12px 16px',
653 marginBottom: '20px',
654 fontWeight: '500',
655 opacity: 0,
656 transition: 'opacity 0.2s ease'
657 });
658
659 $form.prepend($error);
660
661 // Fade in error message
662 setTimeout(function() {
663 $error.css({
664 display: 'block',
665 opacity: 1
666 });
667 }, 10);
668
669 // Scroll to error smoothly
670 setTimeout(function() {
671 $('html, body').animate({
672 scrollTop: $form.offset().top - 100
673 }, 300);
674 }, 50);
675 }
676
677 /**
678 * Show form success message
679 */
680 function showFormSuccess($form, message) {
681 // Remove existing messages
682 $form.find('.yatra-form-error, .yatra-form-success').fadeOut(200, function() {
683 $(this).remove();
684 });
685
686 // Create and insert success message
687 const $success = $('<div class="yatra-form-success" style="display: none;">' + message + '</div>');
688 $success.css({
689 color: '#059669',
690 backgroundColor: '#f0fdf4',
691 border: '1px solid #bbf7d0',
692 borderRadius: '8px',
693 padding: '12px 16px',
694 marginBottom: '20px',
695 fontWeight: '500',
696 opacity: 0,
697 transition: 'opacity 0.2s ease'
698 });
699
700 $form.prepend($success);
701
702 // Fade in success message
703 setTimeout(function() {
704 $success.css({
705 display: 'block',
706 opacity: 1
707 });
708 }, 10);
709 }
710
711 })(jQuery);
712