1/** 2 * Validações, máscaras e helper de binding de formulários do site. 3 * 4 * Carregar via: <script src="/assets/javascript/form-validations.js" defer></script> 5 * 6 * Namespace global `window.VindiForms`: 7 * - validarNome(nome) -> bool 8 * - validarEmail(email) -> bool 9 * - validarCNPJ(cnpj) -> bool (suporta novo CNPJ alfanumérico) 10 * - validarCelular(telefone) -> bool (celular BR, 11 dÃgitos, 9 após DDD) 11 * - validarUrl(valor) -> bool (domÃnio ou URL com/sem http(s)://, exige host com ".") 12 * - normalizarUrl(valor) -> string (antepõe https:// quando ausente) 13 * - mascaraCNPJ(valor) -> string (XX.XXX.XXX/XXXX-XX) 14 * - mascaraCelular(valor) -> string ((XX) 9XXXX-XXXX) 15 * - consultarReceita(cnpj, cb) -> void (consulta ReceitaWS via JSONP) 16 * - bindForm(config) -> void (faz toda a fiação do form) 17 * 18 * Veja docs/formularios.md para exemplos de uso. 19 */ 20window.VindiForms = (function () { 21 'use strict'; 22 23 function validarNome(nome) { 24 if (nome === null || nome === undefined) return false; 25 26 var s = String(nome).trim().replace(/\s+/g, ' '); 27 if (s.length < 3) return false; 28 29 var partes = s.split(' '); 30 if (partes.length < 2) return false; 31 32 var regex = /^[A-Za-zÃ-ÃÃ-öø-ÿ'â\-]{2,}$/; 33 for (var i = 0; i < partes.length; i++) { 34 if (!regex.test(partes[i])) return false; 35 } 36 return true; 37 } 38 39 function validarEmail(email) { 40 if (email === null || email === undefined) return false; 41 var s = String(email).trim(); 42 return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(s); 43 } 44 45 function validarCNPJ(cnpj) { 46 if (cnpj === null || cnpj === undefined) return false; 47 48 cnpj = String(cnpj).toUpperCase().replace(/[^A-Z0-9]/g, ''); 49 50 if (cnpj.length !== 14) return false; 51 if (!/^[A-Z0-9]{12}\d{2}$/.test(cnpj)) return false; 52 if (/^(.)\1{13}$/.test(cnpj)) return false; 53 54 var pesos1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; 55 var pesos2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; 56 57 var soma1 = 0; 58 for (var i = 0; i < 12; i++) { 59 soma1 += (cnpj.charCodeAt(i) - 48) * pesos1[i]; 60 } 61 var dv1 = soma1 % 11 < 2 ? 0 : 11 - (soma1 % 11); 62 if (dv1 !== parseInt(cnpj.charAt(12), 10)) return false; 63 64 var soma2 = 0; 65 for (var j = 0; j < 13; j++) { 66 soma2 += (cnpj.charCodeAt(j) - 48) * pesos2[j]; 67 } 68 var dv2 = soma2 % 11 < 2 ? 0 : 11 - (soma2 % 11); 69 if (dv2 !== parseInt(cnpj.charAt(13), 10)) return false; 70 71 return true; 72 } 73 74 function validarCelular(telefone) { 75 if (telefone === null || telefone === undefined) return false; 76 77 var d = String(telefone).replace(/\D/g, ''); 78 79 if (d.length !== 11) return false; 80 if (d.charAt(0) === '0' || d.charAt(1) === '0') return false; 81 if (d.charAt(2) !== '9') return false; 82 83 return true; 84 } 85 86 /** Aceita domÃnio ou URL com/sem http(s)://; antepõe https:// quando ausente. */ 87 function normalizarUrl(s) { 88 var t = (s || '').trim(); 89 if (!t) return ''; 90 if (/^\/\//.test(t)) return 'https:' + t; 91 if (/^https?:\/\//i.test(t)) return t; 92 return 'https://' + t; 93 } 94 95 function validarUrl(s) { 96 var t = normalizarUrl(s); 97 if (!t) return false; 98 try { 99 var u = new URL(t); 100 if (u.protocol !== 'http:' && u.protocol !== 'https:') return false; 101 if (!u.hostname) return false; 102 return u.hostname.indexOf('.') !== -1; 103 } catch (e) { 104 return false; 105 } 106 } 107 108 function mascaraCNPJ(v) { 109 var raw = String(v).toUpperCase().replace(/[^A-Z0-9]/g, ''); 110 var base = raw.slice(0, 12); 111 var dv = raw.slice(12, 14).replace(/[^0-9]/g, ''); 112 var clean = base + dv; 113 114 if (clean.length <= 2) return clean; 115 if (clean.length <= 5) return clean.slice(0, 2) + '.' + clean.slice(2); 116 if (clean.length <= 8) return clean.slice(0, 2) + '.' + clean.slice(2, 5) + '.' + clean.slice(5); 117 if (clean.length <= 12) return clean.slice(0, 2) + '.' + clean.slice(2, 5) + '.' + clean.slice(5, 8) + '/' + clean.slice(8); 118 return clean.slice(0, 2) + '.' + clean.slice(2, 5) + '.' + clean.slice(5, 8) + '/' + clean.slice(8, 12) + '-' + clean.slice(12); 119 } 120 121 function mascaraCelular(v) { 122 var d = String(v).replace(/\D/g, '').slice(0, 11); 123 if (d.length === 0) return ''; 124 if (d.length <= 2) return '(' + d; 125 if (d.length <= 7) return '(' + d.slice(0, 2) + ') ' + d.slice(2); 126 return '(' + d.slice(0, 2) + ') ' + d.slice(2, 7) + '-' + d.slice(7); 127 } 128 129 /** 130 * Consulta a ReceitaWS via JSONP (não há CORS na API). 131 * Chama onResult(data) com o objeto retornado, ou onResult(null) em erro de rede. 132 * Preserva o novo CNPJ alfanumérico (12 alfanuméricos + 2 dÃgitos); remove só a máscara. 133 */ 134 function consultarReceita(cnpj, onResult) { 135 var clean = String(cnpj).toUpperCase().replace(/[^A-Z0-9]/g, ''); 136 if (clean.length !== 14) { 137 onResult(null); 138 return; 139 } 140 var cbName = '__vfReceita_' + Date.now() + '_' + Math.floor(Math.random() * 1e6); 141 var script = document.createElement('script'); 142 143 function finalizar(data) { 144 try { delete window[cbName]; } catch (e) { window[cbName] = undefined; } 145 if (script.parentNode) script.parentNode.removeChild(script); 146 if (typeof onResult === 'function') onResult(data); 147 } 148 149 window[cbName] = function (data) { finalizar(data || null); }; 150 script.onerror = function () { finalizar(null); }; 151 script.src = 'https://receitaws.com.br/v1/cnpj/' + clean + '?callback=' + cbName; 152 document.body.appendChild(script); 153 } 154 155 var api = { 156 validarNome: validarNome, 157 validarEmail: validarEmail,
158 validarCNPJ: validarCNPJ, 159 validarCelular: validarCelular, 160 validarUrl: validarUrl, 161 normalizarUrl: normalizarUrl, 162 mascaraCNPJ: mascaraCNPJ, 163 mascaraCelular: mascaraCelular, 164 consultarReceita: consultarReceita 165 }; 166 167 function resolveEl(refOrSel) { 168 if (!refOrSel) return null; 169 return typeof refOrSel === 'string' ? document.querySelector(refOrSel) : refOrSel; 170 } 171 172 function resolveFn(refOrName) { 173 if (!refOrName) return null; 174 return typeof refOrName === 'string' ? api[refOrName] : refOrName; 175 } 176 177 // Preserva posição aproximada do cursor ao reaplicar a máscara, evitando 178 // que o caret pule para o fim quando o usuário edita no meio do campo. 179 function aplicarMascara(input, fn) { 180 var posInicial = input.selectionStart; 181 var tamAntes = input.value.length; 182 input.value = fn(input.value); 183 var delta = input.value.length - tamAntes; 184 var novaPos = Math.max(0, posInicial + delta); 185 try { input.setSelectionRange(novaPos, novaPos); } catch (_) { /* type=email etc */ } 186 } 187 188 function bindForm(config) { 189 var formEl = resolveEl(config.form); 190 var submitBtn = resolveEl(config.submitBtn) || 191 (formEl && formEl.querySelector('button[type="submit"], input[type="submit"]')); 192 if (!formEl || !submitBtn) return; 193 194 var isInputSubmit = submitBtn.tagName === 'INPUT'; 195 196 var fields = (config.fields || []) 197 .map(function (f) { 198 var inputEl = resolveEl(f.input); 199 return { 200 input: inputEl, 201 error: resolveEl(f.error), 202 validator: resolveFn(f.validator), 203 mask: resolveFn(f.mask), 204 // Auto-detecta receita check via data-receita-check no input, 205 // emitido pelo include com receita_check=true. 206 receitaCheck: f.receitaCheck === true || 207 (inputEl && inputEl.dataset && inputEl.dataset.receitaCheck === 'true'), 208 receitaState: 'idle', // 'idle' | 'pending' | 'ok' | 'erro' 209 receitaReqId: 0, 210 defaultMsg: '' 211 }; 212 }) 213 .filter(function (f) { return f.input && f.error && f.validator; }); 214 215 if (!fields.length) return; 216 217 fields.forEach(function (f) { f.defaultMsg = (f.error.textContent || '').trim(); }); 218 219 function updateSubmitState() { 220 submitBtn.disabled = fields.some(function (f) { return !f.error.hidden; }); 221 } 222 223 function showError(f, msg) { 224 f.input.classList.add('is-invalid'); 225 f.input.setAttribute('aria-invalid', 'true'); 226 f.error.classList.remove('field-error--info'); 227 f.error.textContent = msg || f.defaultMsg; 228 f.error.hidden = false; 229 updateSubmitState(); 230 } 231 232 function showInfo(f, msg) { 233 f.input.classList.remove('is-invalid'); 234 f.input.removeAttribute('aria-invalid'); 235 f.error.classList.add('field-error--info'); 236 f.error.textContent = msg; 237 f.error.hidden = false; 238 updateSubmitState(); 239 } 240 241 function clearMessage(f) { 242 f.input.classList.remove('is-invalid'); 243 f.input.removeAttribute('aria-invalid'); 244 f.error.classList.remove('field-error--info'); 245 f.error.textContent = f.defaultMsg; 246 f.error.hidden = true; 247 updateSubmitState(); 248 } 249 250 function preencherReceita(data) { 251 var nodes = formEl.querySelectorAll('[data-receita-target]'); 252 for (var i = 0; i < nodes.length; i++) { 253 var key = nodes[i].getAttribute('data-receita-target'); 254 var v = data && data[key]; 255 nodes[i].value = (v == null || v === '') ? '' : String(v); 256 } 257 } 258 259 function limparReceita() { 260 var nodes = formEl.querySelectorAll('[data-receita-target]'); 261 for (var i = 0; i < nodes.length; i++) nodes[i].value = ''; 262 } 263 264 function executarReceitaCheck(f) {
265 f.receitaState = 'pending'; 266 f.receitaReqId += 1; 267 var reqId = f.receitaReqId; 268 showInfo(f, 'Validando CNPJ...'); 269 limparReceita(); 270 271 consultarReceita(f.input.value, function (data) { 272 // Ignora respostas fora de ordem (usuário pode ter editado/colado outro CNPJ). 273 if (reqId !== f.receitaReqId) return; 274 275 // Soft fail: rede, rate limit, CNPJ não encontrado etc. 276 // Não bloqueia o form â submit segue sem enriquecimento. 277 if (!data || data.status === 'ERROR') { 278 f.receitaState = 'ok'; 279 clearMessage(f); 280 return; 281 } 282 // Regra de negócio explÃcita da Receita: bloqueia. 283 if (data.situacao && String(data.situacao).toUpperCase() !== 'ATIVA') { 284 f.receitaState = 'erro'; 285 showError(f, 'Insira um CNPJ que esteja ativo.'); 286 return; 287 } 288 f.receitaState = 'ok'; 289 clearMessage(f); 290 preencherReceita(data); 291 }); 292 } 293 294 fields.forEach(function (f) { 295 f.input.addEventListener('input', function () { 296 if (f.mask) aplicarMascara(f.input, f.mask); 297 if (f.receitaCheck && f.receitaState !== 'idle') { 298 f.receitaState = 'idle'; 299 f.receitaReqId += 1; // invalida qualquer resposta pendente 300 limparReceita(); 301 } 302 if (!f.error.hidden) clearMessage(f); 303 }); 304 305 f.input.addEventListener('blur', function () { 306 if (f.input.value === '') { 307 clearMessage(f); 308 if (f.receitaCheck) { f.receitaState = 'idle'; limparReceita(); } 309 return; 310 } 311 if (!f.validator(f.input.value)) { 312 showError(f); 313 if (f.receitaCheck) { f.receitaState = 'idle'; limparReceita(); } 314 return; 315 } 316 clearMessage(f); 317 if (f.receitaCheck) executarReceitaCheck(f); 318 }); 319 }); 320 321 formEl.addEventListener('submit', function (e) { 322 var firstInvalid = null; 323 fields.forEach(function (f) { 324 if (!f.validator(f.input.value)) { 325 showError(f); 326 if (!firstInvalid) firstInvalid = f.input; 327 return; 328 } 329 if (f.receitaCheck) { 330 // Autofill/submit sem blur: dispara a consulta agora. 331 if (f.receitaState === 'idle') executarReceitaCheck(f); 332 333 if (f.receitaState === 'pending') { 334 showInfo(f, 'Aguarde a validação do CNPJ...'); 335 if (!firstInvalid) firstInvalid = f.input; 336 } else if (f.receitaState === 'erro') { 337 if (!firstInvalid) firstInvalid = f.input; 338 } 339 } 340 }); 341 342 if (firstInvalid) { 343 e.preventDefault(); 344 firstInvalid.focus(); 345 return; 346 } 347 348 if (config.loadingText) { 349 if (isInputSubmit) { 350 submitBtn.dataset.originalText = submitBtn.value; 351 submitBtn.value = config.loadingText; 352 } else { 353 submitBtn.dataset.originalText = submitBtn.textContent; 354 submitBtn.textContent = config.loadingText; 355 } 356 submitBtn.disabled = true; 357 } 358 }); 359 } 360 361 api.bindForm = bindForm; 362 return api; 363})();
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.