PageSourceSearch

https://vindi.com.br/assets/javascript/form-validations.js

js vindi.com.br collected 2026-10-09 22:39:09 UTC 14,272 bytes, 363 lines download raw bytes

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.