https://www.ushandball.org/wp-content/mu-plugins/flykod-custom-login/assets/js/frontend.js?ver=1.0.3
1/** 2 * Custom Login Frontend JavaScript 3 * 4 * Handles form validation, password strength, accessibility features, 5 * and animations for the custom login page. 6 * 7 * @package Flykod_Custom_Login 8 * @version 1.0.0 9 */ 10 11(function() { 12 'use strict'; 13 14 /** 15 * Main Custom Login Class 16 */ 17 class FlykodCustomLogin { 18 constructor() { 19 this.init(); 20 } 21 22 init() { 23 this.setupFormValidation(); 24 this.setupPasswordStrength(); 25 this.setupAccessibility(); 26 this.setupAnimations(); 27 this.setupPasswordToggles(); 28 } 29 30 /** 31 * Setup form validation 32 */ 33 setupFormValidation() { 34 const forms = document.querySelectorAll('.login-form'); 35 36 forms.forEach(form => { 37 // Only prevent submit for password reset forms 38 form.addEventListener('submit', (e) => { 39 if (form.id === 'resetpassform') { 40 this.validateForm(e); 41 } else { 42 // For login form, just show loading state 43 const submitBtn = form.querySelector('input[type="submit"]'); 44 if (submitBtn) { 45 submitBtn.disabled = true; 46 submitBtn.value = 'Processing...'; 47 } 48 } 49 }); 50 }); 51 } 52 53 /** 54 * Validate form on submit 55 * 56 * @param {Event} e Submit event 57 */ 58 validateForm(e) { 59 const form = e.target; 60 const inputs = form.querySelectorAll('input[required]'); 61 let isValid = true; 62 63 inputs.forEach(input => { 64 if (!this.validateField(input)) { 65 isValid = false; 66 } 67 }); 68 69 // Special validation for password reset 70 if (form.id === 'resetpassform') { 71 const pass1 = form.querySelector('#pass1'); 72 const pass2 = form.querySelector('#pass2'); 73 74 if (pass1 && pass2) { 75 if (pass1.value !== pass2.value) { 76 this.showFieldError(pass2, 'Passwords do not match'); 77 isValid = false; 78 } 79 80 if (pass1.value.length < 8) { 81 this.showFieldError(pass1, 'Password must be at least 8 characters'); 82 isValid = false; 83 } 84 } 85 } 86 87 if (!isValid) { 88 e.preventDefault(); 89 this.showFormError(form, 'Please correct the errors below'); 90 } else { 91 // Show loading state 92 const submitBtn = form.querySelector('input[type="submit"]'); 93 if (submitBtn) { 94 submitBtn.disabled = true; 95 const originalValue = submitBtn.value; 96 submitBtn.value = 'Processing...'; 97 submitBtn.dataset.originalValue = originalValue; 98 } 99 } 100 } 101 102 /** 103 * Validate individual field 104 * 105 * @param {HTMLElement} input Input element 106 * @return {boolean} Is valid 107 */ 108 validateField(input) { 109 const value = input.value.trim(); 110 111 if (!value) { 112 this.showFieldError(input, 'This field is required'); 113 return false; 114 } 115 116 // Skip email validation for now (username OR email allowed) 117 // Password validation only for reset forms 118 if (input.type === 'password' && input.name.startsWith('pass')) { 119 if (value.length < 8) {
120 this.showFieldError(input, 'Password must be at least 8 characters'); 121 return false; 122 } 123 } 124 125 this.clearFieldError(input); 126 return true; 127 } 128 129 /** 130 * Show field error 131 * 132 * @param {HTMLElement} input Input element 133 * @param {string} message Error message 134 */ 135 showFieldError(input, message) { 136 this.clearFieldError(input); 137 138 input.classList.add('error'); 139 input.setAttribute('aria-invalid', 'true'); 140 141 const errorDiv = document.createElement('div'); 142 errorDiv.className = 'field-error'; 143 errorDiv.textContent = message; 144 errorDiv.style.color = '#c53030'; 145 errorDiv.style.fontSize = '0.8rem'; 146 errorDiv.style.marginTop = '4px'; 147 errorDiv.setAttribute('role', 'alert'); 148 149 input.parentNode.appendChild(errorDiv); 150 } 151 152 /** 153 * Clear field error 154 * 155 * @param {HTMLElement} input Input element 156 */ 157 clearFieldError(input) { 158 input.classList.remove('error'); 159 input.removeAttribute('aria-invalid'); 160 161 const existingError = input.parentNode.querySelector('.field-error'); 162 if (existingError) { 163 existingError.remove(); 164 } 165 } 166 167 /** 168 * Show form-level error 169 * 170 * @param {HTMLElement} form Form element 171 * @param {string} message Error message 172 */ 173 showFormError(form, message) { 174 let errorDiv = form.querySelector('.form-error'); 175 176 if (!errorDiv) { 177 errorDiv = document.createElement('div'); 178 errorDiv.className = 'form-error login-error'; 179 form.insertBefore(errorDiv, form.firstChild); 180 } 181 182 errorDiv.innerHTML = `<p>${message}</p>`; 183 errorDiv.style.display = 'block'; 184 errorDiv.setAttribute('role', 'alert'); 185 186 // Scroll to error 187 errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' }); 188 } 189 190 /** 191 * Setup password strength indicator 192 */ 193 setupPasswordStrength() { 194 const pass1Input = document.querySelector('#pass1'); 195 if (!pass1Input) return; 196 197 pass1Input.addEventListener('input', (e) => { 198 this.updatePasswordStrength(e.target.value); 199 }); 200 } 201 202 /** 203 * Update password strength indicator 204 * 205 * @param {string} password Password value 206 */ 207 updatePasswordStrength(password) { 208 let strength = 0; 209 let feedback = ''; 210 211 // Length check 212 if (password.length >= 8) strength += 1; 213 if (password.length >= 12) strength += 1; 214 215 // Character variety checks 216 if (/[a-z]/.test(password)) strength += 1; 217 if (/[A-Z]/.test(password)) strength += 1; 218 if (/[0-9]/.test(password)) strength += 1; 219 if (/[^A-Za-z0-9]/.test(password)) strength += 1; 220 221 // Determine strength level 222 let strengthClass = 'weak'; 223 if (strength >= 4) { 224 strengthClass = 'strong'; 225 feedback = 'Strong password'; 226 } else if (strength >= 2) { 227 strengthClass = 'medium'; 228 feedback = 'Medium strength password'; 229 } else { 230 feedback = 'Weak password'; 231 } 232 233 // Update or create strength indicator 234 let indicator = document.querySelector('.password-strength'); 235 if (!indicator) { 236 indicator = document.createElement('div'); 237 indicator.className = 'password-strength'; 238 // Insert AFTER the password-input-wrapper, not inside it 239 const pass1Wrapper = document.querySelector('#pass1').closest('.password-input-wrapper'); 240 if (pass1Wrapper && pass1Wrapper.parentNode) { 241 pass1Wrapper.parentNode.insertBefore(indicator, pass1Wrapper.nextSibling); 242 } 243 } 244 245 indicator.textContent = feedback; 246 indicator.className = `password-strength ${strengthClass}`; 247 indicator.setAttribute('aria-live', 'polite'); 248 } 249 250 /** 251 * Setup accessibility features 252 */ 253 setupAccessibility() { 254 // Add ARIA labels 255 const inputs = document.querySelectorAll('input');
256 inputs.forEach(input => { 257 if (!input.getAttribute('aria-label') && input.type !== 'hidden') { 258 const label = document.querySelector(`label[for="${input.id}"]`); 259 if (label) { 260 input.setAttribute('aria-label', label.textContent.trim()); 261 } 262 } 263 }); 264 265 // Keyboard navigation 266 document.addEventListener('keydown', (e) => { 267 if (e.key === 'Enter' && e.target.tagName === 'INPUT' && e.target.type !== 'submit') { 268 const form = e.target.closest('form'); 269 if (form) { 270 const submitBtn = form.querySelector('input[type="submit"]'); 271 if (submitBtn && !submitBtn.disabled) { 272 submitBtn.click(); 273 } 274 } 275 } 276 }); 277 } 278 279 /** 280 * Setup password toggle buttons 281 */ 282 setupPasswordToggles() { 283 const passwordToggles = document.querySelectorAll('.password-toggle'); 284 285 passwordToggles.forEach(toggle => { 286 const wrapper = toggle.closest('.password-input-wrapper'); 287 if (!wrapper) return; 288 289 const passwordInput = wrapper.querySelector('input[type="password"], input[type="text"]'); 290 if (!passwordInput) return; 291 292 toggle.addEventListener('click', () => { 293 const isShowing = passwordInput.type === 'text'; 294 295 if (isShowing) { 296 passwordInput.type = 'password'; 297 toggle.classList.remove('showing'); 298 toggle.setAttribute('aria-label', 'Show password'); 299 this.announceToScreenReader('Password hidden'); 300 } else { 301 passwordInput.type = 'text'; 302 toggle.classList.add('showing'); 303 toggle.setAttribute('aria-label', 'Hide password'); 304 this.announceToScreenReader('Password visible'); 305 } 306 }); 307 308 // Keyboard support 309 toggle.addEventListener('keydown', (e) => { 310 if (e.key === 'Enter' || e.key === ' ') { 311 e.preventDefault(); 312 toggle.click(); 313 } 314 }); 315 }); 316 } 317 318 /** 319 * Setup animations 320 */ 321 setupAnimations() { 322 // Add entrance animations 323 const elements = document.querySelectorAll('.form-content > *'); 324 elements.forEach((element, index) => { 325 element.style.opacity = '0'; 326 element.style.transform = 'translateY(20px)'; 327 element.style.transition = 'opacity 0.6s ease, transform 0.6s ease'; 328 329 setTimeout(() => { 330 element.style.opacity = '1'; 331 element.style.transform = 'translateY(0)'; 332 }, index * 100); 333 }); 334 } 335 336 /** 337 * Announce message to screen readers 338 * 339 * @param {string} message Message to announce 340 */ 341 announceToScreenReader(message) { 342 let announcement = document.getElementById('flykod-sr-announcement'); 343 if (!announcement) { 344 announcement = document.createElement('div'); 345 announcement.id = 'flykod-sr-announcement'; 346 announcement.setAttribute('aria-live', 'polite'); 347 announcement.setAttribute('aria-atomic', 'true'); 348 announcement.style.position = 'absolute'; 349 announcement.style.left = '-10000px'; 350 announcement.style.width = '1px'; 351 announcement.style.height = '1px'; 352 announcement.style.overflow = 'hidden'; 353 document.body.appendChild(announcement); 354 } 355 356 announcement.textContent = message; 357 358 setTimeout(() => { 359 announcement.textContent = ''; 360 }, 1000); 361 } 362 } 363 364 // Initialize when DOM is loaded 365 if (document.readyState === 'loading') { 366 document.addEventListener('DOMContentLoaded', () => { 367 new FlykodCustomLogin(); 368 }); 369 } else { 370 new FlykodCustomLogin(); 371 } 372 373})(); 374
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.