Programação Por Equipe DevBras

Capa do artigo: Máscara de telefone e CEP que aceita colar e guarda só os dígitos

Máscara de telefone e CEP que aceita colar e guarda só os dígitos

Monte campos de telefone e CEP que aceitam "+55 (11) 98888-7777" colado, formatam sem travar o teclado e mandam ao servidor só os números, validados de novo lá.

O cliente copia o próprio número da agenda do celular, "+55 11 98888-7777", e cola no campo de telefone do checkout. O campo mostra "(55) 1198-8887" e para de aceitar dígitos. Ele apaga, digita de novo, erra um número, e o entregador liga para um telefone que não existe. A resposta curta: a máscara é só visual; o que importa são os dígitos. Deixe a pessoa digitar ou colar qualquer coisa, extraia os números, normalize e formate quando ela sai do campo. No servidor, faça a mesma extração e a validação de verdade.

O exemplo deste guia é o checkout de uma loja online com dois campos: telefone para o entregador e CEP para o frete.

Por que a máscara clássica atrapalha

A maioria dos scripts de máscara foi escrita para quem digita um número de cada vez, da esquerda para a direita. Eles reagem a cada tecla, inserem o parêntese e o traço na posição esperada e bloqueiam qualquer tecla que não seja número. Funciona até a pessoa fazer algo diferente.

Cola é o caso mais comum. O texto colado chega inteiro, com espaço, sinal de mais e código do país, e o script tenta encaixar treze dígitos num molde de onze. Corrigir um dígito no meio é o segundo caso: a máscara reformata tudo e joga o cursor para o fim. O preenchimento automático do navegador é o terceiro, porque ele preenche o valor de uma vez, sem disparar as teclas que o script esperava.

Há ainda o problema do formato brasileiro. Celular tem 11 dígitos com DDD, fixo tem 10. A máscara que decide o formato no meio da digitação não sabe, no oitavo dígito, qual dos dois a pessoa está escrevendo.

Como montar campos que aceitam qualquer entrada

  1. Use o tipo e os atributos certos: type="tel" para telefone e type="text" com inputmode="numeric" para CEP. Os dois abrem teclado numérico no celular.
  2. Informe o preenchimento automático com autocomplete="tel-national" e autocomplete="postal-code".
  3. Não bloqueie teclas. Deixe entrar espaço, parêntese, traço e sinal de mais.
  4. Quando a pessoa sai do campo, extraia os dígitos, tire o código do país se ele veio, e formate.
  5. Se o número de dígitos não fecha, mostre a mensagem ao lado do campo. Não apague o que ela escreveu.
  6. No servidor, repita a extração, valide o tamanho e grave só os dígitos.

O HTML

<label for="telefone">Celular ou telefone com DDD</label>
<input type="tel" id="telefone" name="telefone" autocomplete="tel-national"
       aria-describedby="telefone-erro">
<p id="telefone-erro" hidden></p>

<label for="cep">CEP</label>
<input type="text" id="cep" name="cep" inputmode="numeric" autocomplete="postal-code"
       aria-describedby="cep-erro">
<p id="cep-erro" hidden></p>

Não há maxlength. Um limite de 15 caracteres, que parece suficiente para "(11) 98888-7777", corta o "+55 11 98888-7777" colado no meio, porque ele tem 17. Também não há type="number": CEP começa com zero em boa parte de São Paulo, como 01310-100, e campo numérico pode descartar o zero, além de mostrar setas de aumentar e diminuir que não fazem sentido para código.

O JavaScript

function soDigitos(texto) {
  return texto.replace(/\D/g, '');
}

function normalizarTelefone(texto) {
  let d = soDigitos(texto);
  if ((d.length === 12 || d.length === 13) && d.startsWith('55')) {
    d = d.slice(2);
  }
  return d;
}

function formatarTelefone(d) {
  if (d.length === 11) return '(' + d.slice(0, 2) + ') ' + d.slice(2, 7) + '-' + d.slice(7);
  if (d.length === 10) return '(' + d.slice(0, 2) + ') ' + d.slice(2, 6) + '-' + d.slice(6);
  return d;
}

function formatarCep(d) {
  return d.length === 8 ? d.slice(0, 5) + '-' + d.slice(5) : d;
}

function ligarCampo(campo, normalizar, formatar, tamanhos, mensagem) {
  const erro = document.getElementById(campo.id + '-erro');
  campo.addEventListener('blur', function () {
    if (campo.value.trim() === '') return;
    const d = normalizar(campo.value);
    const valido = tamanhos.includes(d.length);
    if (valido) campo.value = formatar(d);
    campo.setAttribute('aria-invalid', valido ? 'false' : 'true');
    erro.hidden = valido;
    erro.textContent = valido ? '' : mensagem;
  });
}

ligarCampo(document.getElementById('telefone'), normalizarTelefone, formatarTelefone,
  [10, 11], 'Informe o DDD e o número, com 10 ou 11 dígitos.');
ligarCampo(document.getElementById('cep'), soDigitos, formatarCep,
  [8], 'O CEP tem 8 dígitos.');

Siga o exemplo do começo. A pessoa cola "+55 11 98888-7777". Ao sair do campo, soDigitos devolve 5511988887777, com 13 dígitos. Como começa com 55 e tem 13, o código do país sai e sobra 11988887777, com 11. O campo passa a mostrar "(11) 98888-7777". A checagem do tamanho antes de cortar o 55 importa: existe DDD 55, no Rio Grande do Sul, e um número de lá tem 10 ou 11 dígitos, então não é confundido.

Formatar ao sair do campo, e não a cada tecla, evita o cursor pulando para o fim quando a pessoa corrige um dígito no meio. Se você quer formatar durante a digitação, precisa recalcular a posição do cursor depois de cada mudança, o que é bem mais código e bem mais fácil de errar.

No servidor

<?php
function soDigitos(string $texto): string
{
    return preg_replace('/\D+/', '', $texto);
}

$telefone = soDigitos($_POST['telefone'] ?? '');
if ((strlen($telefone) === 12 || strlen($telefone) === 13) && str_starts_with($telefone, '55')) {
    $telefone = substr($telefone, 2);
}
if (!in_array(strlen($telefone), [10, 11], true)) {
    $erros['telefone'] = 'Informe o DDD e o número, com 10 ou 11 dígitos.';
}

$cep = soDigitos($_POST['cep'] ?? '');
if (strlen($cep) !== 8) {
    $erros['cep'] = 'O CEP tem 8 dígitos.';
}

Aqui strlen é seguro, porque depois da extração só sobram dígitos ASCII. A função str_starts_with existe a partir do PHP 8. Grave 11988887777 e 01310100 numa coluna de texto, nunca numérica, para preservar o zero à esquerda. A formatação com parêntese e traço é responsabilidade de quem exibe. Para a diferença entre máscara e validação em outros campos numéricos, veja campo numérico: a máscara não é a validação.

Erros comuns

  • Bloquear tudo que não é número. A cola com espaço ou parêntese é recusada inteira, e a pessoa precisa digitar dígito por dígito.
  • maxlength do tamanho formatado. O número colado com +55 é cortado no meio e o pedido sai com telefone errado.
  • Usar type="number" para CEP. O zero à esquerda some e o frete é calculado para outro lugar, ou não é calculado.
  • Gravar o texto formatado. O banco fica com "(11) 98888-7777", "11 98888 7777" e "11988887777" para o mesmo número, e a busca por telefone no painel não acha nada.
  • Coluna numérica para telefone ou CEP. O zero inicial é perdido na gravação, sem erro nenhum.
  • Validar só no navegador. Aplicativo, importação e requisição direta gravam qualquer coisa.

Checklist

  • Colar "+55 (11) 98888-7777" resulta em "(11) 98888-7777" ao sair do campo.
  • Os campos abrem teclado numérico no celular e aceitam preenchimento automático.
  • Nenhum maxlength corta a cola com código do país.
  • O CEP 01310-100 chega ao banco com o zero.
  • Corrigir um dígito no meio não joga o cursor para o fim.
  • O servidor extrai dígitos, valida o tamanho e grava só números, em coluna de texto.
  • Erro de tamanho aparece ao lado do campo, sem apagar o que foi digitado.

Cole o seu próprio número, com +55 e parênteses, no campo de telefone do seu formulário. Ele ficou inteiro? E no banco, a coluna guarda os dígitos limpos ou cada pedido com um formato diferente?

Avalie este artigo

Referência: http://www.vivaolinux.com.br/script/Mascara-JavaScript-para-Campos-de-Telefone-Celular-e-CEP/