Editor de texto
Mostra o resultado enquanto se escreve: quem preenche uma descrição no sistema vê negrito em
negrito, e não marcação. Com tabela e bloco de código. O <textarea> original continua no
formulário guardando o HTML, então name, required e o POST funcionam sem mudar a view.
Exemplos
O mesmo <textarea>, com atributos ou opções diferentes.
Barra enxuta
toolbar escolhe os botões e a ordem — aqui, só o que um comentário precisa.
Vazio, mais baixo
data-placeholder e data-min-height="5rem".
Com erro
aria-invalid="true" no textarea pinta a moldura — o Django 5 já escreve o atributo.
Escreva a justificativa.
A peneira, ao vivo
Tucano.sanitize() sobre um HTML com onerror, javascript: e <script>.
Como usar
Marque o <textarea> e ele inicializa sozinho, inclusive o que chegar depois por HTMX.
O conteúdo inicial é o HTML guardado, e também passa pela peneira — ele pode ter vindo do banco.
<textarea name="description" data-tuc-editor>{{ form.description.value|default:"" }}</textarea>No formulário do Django
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = ["description"]
widgets = {
"description": forms.Textarea(attrs={
"data-tuc-editor": "",
"data-placeholder": "Descreva o escopo...",
"data-min-height": "12rem",
}),
}Em JavaScript
Para escolher a barra e o tamanho da tabela, que não existem como atributo, ou para ler e escrever o valor.
const ed = new Tucano.Editor('#description', {
toolbar: ['bold', 'italic', 'list', 'link', 'table'],
table: { rows: 4, cols: 2 }, // linhas contando o cabeçalho
});
ed.getValue(); // HTML já peneirado; '' se vazio
ed.setValue('<p>Texto novo</p>'); // peneira, pinta o código e atualiza o textarea
ed.apply('bold'); // o mesmo que o botão da barra
ed.inTable('rowBelow'); // operação na célula onde está o cursor
ed.destroy(); // devolve o textarea originalA barra
Um botão aceso diz que o cursor está dentro daquela formatação — e que clicar de novo desfaz. Os nomes
abaixo são os que a opção toolbar aceita, na ordem do padrão.
| Nome | Botão | O que faz |
|---|---|---|
bold italic underline | Negrito, Itálico, Sublinhado | Marca o trecho; Ctrl/Cmd+B, +I, +U |
title subheading | Título, Subtítulo | Transforma o bloco em <h2> ou <h3>; de novo, volta a parágrafo |
list numbered | Lista, Lista numerada | <ul> e <ol> |
left center right justify | Alinhamentos | Alinha o bloco |
quote | Citação | <blockquote>; de novo, volta a parágrafo |
code | Código | Código no meio da frase ou bloco de código — veja abaixo |
link | Link | Abre a caixa de endereço; Ctrl/Cmd+K |
table | Inserir tabela | Tabela com cabeçalho no lugar do cursor |
clear | Limpar formatação | Tira a formatação de texto do trecho selecionado |
A barra fica numa linha só e rola quando não cabe, em vez de quebrar em duas: assim a altura não muda e quem procura um botão sabe que ele não trocou de fileira.
Colar e arrastar entram sempre como texto puro
É o que evita o HTML do Word e do Google Docs, e o de outra página arrastado para dentro, com tabelas de layout e estilos embutidos, que é onde editor caseiro quebra primeiro. Você perde a formatação da origem e ganha um documento que continua sendo o seu.
O Ctrl+Z é o do navegador
A formatação passa por execCommand, que está deprecado e é usado assim mesmo: é o único caminho com suporte universal e o único que se integra ao desfazer nativo. Reimplementar à mão seria reimplementar o Ctrl+Z junto. A exceção é tirar um bloco de código, que sai do histórico nessa ação só.
Tabela
O botão insere uma tabela com cabeçalho — tabela de sistema quase sempre tem um, e sem ele a primeira linha de dados acaba servindo de título. Com o cursor numa célula, aparece uma segunda barra só para ela.
| Operação | Nome em inTable() | Detalhe |
|---|---|---|
| Inserir linha acima / abaixo | rowAbove rowBelow | A partir do cabeçalho, a linha nova vai para o corpo |
| Inserir coluna à esquerda / à direita | colBefore colAfter | Célula nova é th no cabeçalho e td no corpo |
| Excluir linha | deleteRow | Na última linha, some a tabela inteira, em vez de sobrar a moldura |
| Excluir coluna | deleteColumn | Idem, na última coluna |
| Excluir tabela | deleteTable |
Na tabela, Tab anda para a próxima célula e Shift+Tab volta; na última célula, Tab
cria uma linha nova — dá para preencher sem tirar as mãos do teclado. Inserir tabela com o cursor dentro de
outra põe a nova depois dela, e não aninhada. A barra de tabela só aparece quando faz falta: sempre visível,
encheria a barra principal de botões inúteis na maior parte do tempo.
Fora do escopo
Mesclar células e redimensionar coluna — é o que faz editor virar projeto. As colunas têm largura fixa de propósito: com largura automática, o texto redimensionaria a coluna a cada tecla e a linha inteira dançaria.
Bloco de código
O mesmo botão faz as duas coisas, pelo que está selecionado: trecho dentro de uma linha vira
<code> no meio da frase; seleção que atravessa linhas vira <pre><code>,
que é o elemento que preserva quebra e recuo. Com o cursor dentro, o botão desfaz.
O bloco é colorido enquanto se escreve — comentário, texto entre aspas, número, tag, atributo, palavra-chave e
chaves de template. A cor é só exibição: a peneira dissolve <span>, então nada dela chega ao valor
salvo, e nem deveria, porque cor é decisão de quem exibe.
Link
O endereço é pedido num modal da própria biblioteca, e não no prompt do navegador — que
aparece fora do desenho da página, ignora o tema e não se estiliza.
Selecione o texto e use o botão ou Ctrl/Cmd+K. Enter no campo confirma. Com o cursor
num link existente, a caixa abre com o endereço dele e ganha "Remover". Só passa endereço que começa com
http:, https:, mailto:, tel:, # ou / —
o resto, javascript: e //outro-site.com incluídos, perde o link e mantém o texto.
Endereço digitado sem esquema, como exemplo.com, ganha https:// na frente. Todo link salvo sai com
target="_blank" e rel="noopener noreferrer".
Variáveis
Um texto que vira mensagem para cada pessoa tem trechos que mudam: o nome de quem recebe, o prazo, o
título da tarefa. Declare essas variáveis e o editor ganha um botão na barra e uma lista ao digitar
{ — ninguém precisa decorar nome nem fechar chaves na mão.
Clique no { } da barra, ou digite { no texto e comece a escrever o nome.
new Tucano.Editor('#message', {
variables: [
{ name: 'nome', label: 'Nome do responsável', example: 'Junior' },
{ name: 'tarefa', label: 'Título da tarefa', example: 'Gravar o vídeo' },
{ name: 'prazo', label: 'Prazo', example: '21/09' },
],
});O que entra no texto é {{nome}}, texto puro: o seu template no servidor continua trocando pelos dados
como já fazia, e copiar, colar e desfazer seguem funcionando. O label é o que aparece na lista; o
example fica guardado para a prévia que o seu projeto monta.
| O que faz | Como |
|---|---|
| Abrir a lista pela barra | Botão { }, que só existe quando há variáveis |
| Abrir enquanto escreve | Digite {; as letras seguintes filtram, e o foco não sai do texto |
| Escolher | Clique, ou ↓ e Enter. O { digitado sai junto |
| Desistir | Esc fecha a lista e deixa você digitando |
| Achar erro de digitação | editor.unknownVariables() devolve o que está no texto e não na lista |
No texto, a variável ganha um fundo leve para se distinguir do resto, e a que não está na lista sai no tom de erro — o aviso aparece onde o erro está. É pintura do navegador sobre o trecho, e não marcação no conteúdo: o valor salvo continua exatamente o que você escreveu. Em navegador sem essa pintura (Safari abaixo da 17.2, Firefox abaixo da 140) o texto aparece sem fundo, e mais nada muda.
Dentro de um bloco de código a lista não abre, e o que estiver escrito ali fica fora do
unknownVariables(): ali se escreve código — inclusive o {{ nome }} de um template, como
exemplo — e uma lista pulando na frente a cada chave atrapalharia.
Sem variables nada muda: não há botão, e { continua sendo só uma chave.
Valor e formulário
Quem guarda o valor é o <textarea>, escondido dentro do editor. A cada alteração ele
recebe o HTML peneirado e dispara input e change nativos.
POST description = <h2>Escopo do contrato</h2><p>Levantamento com <strong>relatório</strong>.</p>document.querySelector('#description').addEventListener('change', (e) => {
e.target.value; // o HTML que vai no POST
});O editor não dispara tucano:change: o valor é texto de formulário, e os eventos nativos do textarea já
servem ao hx-trigger="change" do HTMX e à validação.
Editor vazio vale vazio: o textarea recebe '', e não um parágrafo em branco — então required
barra o envio, e o aviso do navegador leva o foco à área. O reset do formulário devolve o editor ao
conteúdo de origem.
A peneira
A saída passa por uma lista fechada de tags a cada leitura, e não só no que foi digitado: o navegador tem liberdade para marcar como quiser ao executar um comando, e o resultado precisa caber no que o editor promete.
| Entrada | Sai |
|---|---|
p br h2 h3 strong em u s ul ol li blockquote code pre a table thead tbody tr th td | Como estão, sem atributo |
b i | Viram strong e em |
href de a | Só com destino aceitável; senão o link some e o texto fica |
text-align | Só left, center, right ou justify, em p, h2, h3, li, blockquote, th e td — reescrito do zero, nunca o style que veio |
script style iframe object | Somem com o conteúdo |
Qualquer outra tag (div, span, font, h1...) | Perde a tag e mantém o texto |
Qualquer outro atributo (onclick, onerror, class...) | Some |
Tucano.sanitize('<p onclick="x()">Oi <img src=x onerror="steal()"><b>você</b></p>');
// '<p>Oi <strong>você</strong></p>'Sanitize de novo no servidor, antes de publicar
A peneira protege o editor, não a publicação. O HTML chega por POST, e ninguém garante que veio deste editor — qualquer um monta a requisição à mão. Passe o valor por um sanitizador do lado do servidor, com a mesma lista de tags, antes de exibir com |safe.
import nh3
TAGS = {"p", "br", "h2", "h3", "strong", "em", "u", "s", "ul", "ol", "li",
"blockquote", "code", "pre", "a", "table", "thead", "tbody", "tr", "th", "td"}
def sanitize(html):
return nh3.clean(html, tags=TAGS, attributes={"a": {"href"}},
url_schemes={"http", "https", "mailto", "tel"})Um exemplo com o nh3; qualquer sanitizador com lista fechada serve. Para manter o alinhamento, permita também o style com só text-align nos blocos.
Exibir o que foi salvo
O que é salvo é HTML sem classe nenhuma — de propósito, para servir a qualquer servidor e sobreviver a
uma troca de front. O preço é que, publicado, ele herdaria o estilo da sua página. Envolver a saída em
.tuc-prose devolve exatamente a aparência que a pessoa viu ao escrever.
<div class="tuc-prose">{{ project.description|safe }}</div>Escopo do contrato
Levantamento em campo com relatório fotográfico e medição das unidades.
| Etapa | Prazo | Valor |
|---|---|---|
| Vistoria | 5 dias | R$ 2.400 |
| Medição | 10 dias | R$ 5.800 |
npm install tucano
npm run build # gera o dist
O prazo conta a partir da assinatura.
As duas aparências não têm como divergir
As regras de .tuc-prose são as mesmas da área de edição, compartilhadas no CSS. Elas também se defendem do CSS de quem hospeda: table { display: block } e th { text-transform: uppercase } são receitas comuns em projeto, e herdadas aqui desmontariam a tabela e mentiriam sobre o que foi escrito.
Tabela com muitas colunas não espreme o texto: cada coluna tem uma largura mínima, e a tabela que passa da largura
rola na horizontal, sozinha, tanto no editor quanto no .tuc-prose. A caixa que rola é só exibição — o
init() a põe em volta da tabela, e o HTML salvo continua sem ela.
Código colorido e copiar
O init() pinta cada <pre><code> dentro de .tuc-prose e põe um botão de
copiar no canto — inclusive no que chegar depois por HTMX. Passe o mouse no bloco acima para ver.
- O botão aparece no hover e no foco: permanente, ele competiria com o código, que é o que a pessoa veio ler; mas continua no
Tab, para quem navega por teclado. Em tela de toque fica sempre visível, mais discreto. - Copia o texto cru, sem as marcações de cor. Fora de contexto seguro, onde a área de transferência não existe, ele seleciona o bloco e deixa o
Ctrl+Ca um toque. - O destacador não conhece linguagem nenhuma, de propósito: reconhece o que aparece em quase todas, o que cobre qualquer linguagem por 2 KB. As cores são os tokens
--tuc-tok-*, com paleta própria no tema escuro.
Tucano.highlight('const x = 1;'); // HTML com <span class="tuc-tok-..."> para colorir à mão
Tucano.init(fragment); // pinta e põe o copiar no HTML inserido por outro caminhoTeclado e acessibilidade
A área é um role="textbox" com aria-multiline; a barra é um
role="toolbar", e cada botão tem nome e aria-pressed acompanhando a formatação sob o cursor.
| Tecla | Ação |
|---|---|
Ctrl/Cmd+B +I +U | Negrito, itálico, sublinhado |
Ctrl/Cmd+K | Inserir ou editar link |
Ctrl/Cmd+Z | Desfaz, pelo histórico do navegador |
Tab Shift+Tab | Numa tabela, próxima e anterior célula; na última, cria linha |
Enter | Na caixa de link, confirma |
Esc | Na caixa de link, fecha sem aplicar |
Fora de uma tabela, Tab sai do editor, como em qualquer campo. Aplicar formatação não arrasta a página:
o foco volta à área sem rolar.
API
Gerada do código a cada build — se algo não está aqui, não existe.
[data-tuc-editor]new Tucano.Editor(alvo, opcoes)data-min-height data-placeholderinTable apply openVariables insertVariable unknownVariables getValue setValue destroyOpções
| Opção | Padrão | Para quê |
|---|---|---|
toolbar | ['bold', 'italic', 'underline', 'title', 'subheading' | |
table | { rows: 3, cols: 3 } | |
minHeight | '9rem' | |
placeholder | '' | |
variables | null |