/** * Real-time password validation hints — checks length, uppercase, digit, special char. */ import { warnOnce, resolveHost } from '../core/app-dom.js'; const rules = { min: (value, min) => value.length >= min, upper: (value) => /[A-Z]/.test(value), lower: (value) => /[a-z]/.test(value), number: (value) => /\d/.test(value), symbol: (value) => /[^a-zA-Z0-9]/.test(value), email: (value, _min, email) => { if (!email) {return true;} return !value.toLowerCase().includes(email.toLowerCase()); }, match: (_value, _min, _email, confirm) => { if (!confirm) {return false;} return true; } }; export function initPasswordHints(root = document, options = {}) { const selector = String(options.selector || '[data-password-hints]').trim() || '[data-password-hints]'; const host = resolveHost(root); const containers = Array.from(host.querySelectorAll(selector)); if (!containers.length) { return { destroy: () => {} }; } const cleanupFns = []; containers.forEach((container) => { if (!(container instanceof HTMLElement)) {return;} if (container.dataset.passwordHintsBound === '1') {return;} const passwordSelector = container.dataset.passwordInput; const confirmSelector = container.dataset.confirmInput; const emailSelector = container.dataset.emailInput; const scope = document; const passwordInput = passwordSelector ? scope.querySelector(passwordSelector) : null; const confirmInput = confirmSelector ? scope.querySelector(confirmSelector) : null; const emailInput = emailSelector ? scope.querySelector(emailSelector) : null; if (passwordSelector && !passwordInput) { warnOnce('UI_EL_MISSING', `Missing password input: ${passwordSelector}`, { module: 'password-hints' }); } if (confirmSelector && !confirmInput) { warnOnce('UI_EL_MISSING', `Missing confirm input: ${confirmSelector}`, { module: 'password-hints' }); } if (emailSelector && !emailInput) { warnOnce('UI_EL_MISSING', `Missing email input: ${emailSelector}`, { module: 'password-hints' }); } const minLength = Number.parseInt(container.dataset.minLength || '12', 10); const items = container.querySelectorAll('[data-rule]'); const setState = (item, state) => { item.classList.remove('is-valid', 'is-invalid'); if (state === 'valid') {item.classList.add('is-valid');} if (state === 'invalid') {item.classList.add('is-invalid');} }; const evaluate = () => { const password = passwordInput?.value ?? ''; const confirm = confirmInput?.value ?? ''; const email = emailInput?.value ?? ''; const hasValue = password.length > 0 || confirm.length > 0; items.forEach((item) => { const rule = item.dataset.rule; if (!rule) {return;} if (!hasValue) { setState(item, 'neutral'); return; } if (rule === 'match') { if (password === '' || confirm === '') { setState(item, 'neutral'); return; } setState(item, password === confirm ? 'valid' : 'invalid'); return; } const check = rules[rule]; if (!check) {return;} const ok = check(password, minLength, email, confirm); setState(item, ok ? 'valid' : 'invalid'); }); }; if (passwordInput) { passwordInput.addEventListener('input', evaluate); cleanupFns.push(() => passwordInput.removeEventListener('input', evaluate)); } if (confirmInput) { confirmInput.addEventListener('input', evaluate); cleanupFns.push(() => confirmInput.removeEventListener('input', evaluate)); } if (emailInput) { emailInput.addEventListener('input', evaluate); cleanupFns.push(() => emailInput.removeEventListener('input', evaluate)); } evaluate(); container.dataset.passwordHintsBound = '1'; cleanupFns.push(() => { delete container.dataset.passwordHintsBound; }); }); const destroy = () => { cleanupFns.forEach((cleanup) => cleanup()); }; return { destroy }; }