Um editor de texto rich text (WYSIWYG) permite que os usuários manipulem conteúdo com formatações variadas, estilos e elementos multimídia de forma visual e direta. Diferente de componentes simples como o <input>, esses editores oferecem flexibilidade para criar estruturas complexas que incluem tabelas, blocos de código, fórmulas matemáticas e anexos.
A Motivação por Trás do Desenvolvimento do Zero
Embora existam soluções robustas como Quill, Slate e ProseMirror, a maioria das discussões foca na camada de aplicação e não no design fundamental da engine. Desenvolver um editor do zero permite explorar a interseção entre DSLs (Domain Specific Languages) e a manipulação direta do DOM, um conceito que se assemelha muito ao design de plataformas No-Code.
O foco principal na construção de um motor de edição moderno reside em entender como o estado do dado se sincroniza com a visualização do navegador, especialmente lidando com as idiossincrasias do motor de renderização do Chrome.
O Papel Crítico dos Caracteres de Largura Zero
No desenvolvimento de editores baseados em contenteditable, o caractere \u200B (Zero Width Space) é uma ferramenta essencial. Ele atua como um marcador invisível que resolve problemas de posicionamento de cursor e renderização que o HTML puro não consegue gerenciar sozinho.
Por exemplo, para manter o foco do cursor em uma linha vazia ou garantir que o cursor possa ser posicionado antes ou depois de um elemento não editável (como uma tag de menção ou um widget), inserimos esses caracteres invisíveis:
<!-- Exemplo de estrutura de linha com suporte a cursor -->
<div class="editor-row">
<span data-text-node="true">​</span>
<span contenteditable="false" class="mention-tag">@usuario</span>
<span data-text-node="true">​</span>
</div>
Sem esses marcadores, o navegador muitas vezes falha em colocar o cursor em nós de texto adjacentes a elementos display: inline-block, ou colapsa a altura de elementos vazios, tornando a interação do usuário frustrante.
Design de Estruturas de Dados: Árvores vs. Listas Lineares
A escolha da estrutura de dados impacta diretamente na complexidade de algoritmos de busca, desfazer/refazer (Undo/Redo) e colaboração em tempo real.
1. Estruturas Aninhadas (Modelos Baseados em JSON)
Frameworks como o Slate utilizam uma árvore que espelha o DOM. Embora intuitivo, operações em árvores profundas exigem algoritmos complexos de normalização para evitar caminhos "sujos" ou estruturas inválidas após colagens de HTML externo.
const documentoHieraquico = [
{
tipo: 'paragrafo',
filhos: [
{ texto: 'Texto em ' },
{ texto: 'negrito', bold: true }
]
}
];
2. Estruturas Lineares (Modelos Baseados em Delta)
Inspirado pelo Quill, o uso de uma lista plana de operações (Deltas) simplifica a aplicação de transformações operacionais (OT). Em vez de navegar em uma árvore, tratamos o documento como uma série de inserções e retenções com atributos associados.
// Exemplo de uma estrutura Delta simplificada
const operacoesDocumento = [
{ insert: 'Texto formatado' },
{
insert: '\n',
attributes: { cabecalho: 1 }
},
{
insert: 'Item de lista',
attributes: { list: 'bullet' }
}
];
A vantagem da estrutura linear é a idempotência em operações de formatação. Se aplicarmos um estilo de citação a um bloco que já é uma lista, o modelo plano lida com isso apenas atualizando o mapa de atributos (attrs), enquanto modelos aninhados precisariam decidir qual nó envolve qual.
Estratégias de Implementação e Interação com o Navegador
O desenvolvimento de editores é geralmente classificado em três níveis de complexidade:
- L0 (ExecCommand): Utiliza o comando nativo do navegador. É simples, mas gera um HTML inconsistente entre diferentes browsers e oferece pouco controle programático.
- L1 (Model-View-Controller): Utiliza
contenteditableapenas como superfície de renderização e captura de eventos. O estado é mantido em um modelo de dados customizado que intercepta entradas (teclado, colagem) e re-renderiza o DOM conforme necessário. - L2 (Canvas/Renderização Própria): Abandona o DOM para a renderização de texto, desenhando caracteres diretamente em um
<canvas>. Isso garante consistência absoluta (como no Google Docs), mas exige a implementação manual de sistemas complexos de tipografia, seleção e acessibilidade.
Para a maioria das aplicações modernas, a abordaegm L1 é a mais equilibrada. Ela permite aproveitar as capacidades nativas de acessibilidade e corretores ortográficos do navegador, enquanto mantém o controle total sobre a integridade dos dados através de um fluxo unidirecional de dados.
// Lógica básica de um comando controlado
function aplicarNegrito(editor) {
const { selecao, modelo } = editor;
if (selecao.isCollapsed) return;
// Em vez de document.execCommand('bold'),
// alteramos o estado interno:
modelo.updateRange(selecao.range, {
attributes: { bold: true }
});
// O motor de renderização atualiza o DOM baseado no novo estado
editor.syncView();
}
Ao construir um editor, o desafio real não é apenas exibir o texto, mas gerenciar os estados de transição. Isso inclui lidar com a composição de caracteres (IME) para idiomas orientais, normalizar o comportamento da tecla 'Enter' e garantir que a serialização para formatos como Markdown ou PDF seja fiel ao modelo de dados central.