Note Editor
Editor de texto rico para as anotações dos alunos, construído sobre o Tiptap.
O modelo de documento e o schema ficam em @medway-ui/notes-core, compartilhados
pelas duas plataformas abaixo: uma nota criada em uma abre sem conversão na
outra.
Exemplos
Básico
Título
Uma área de título fica acima do conteúdo, com estilo de topo da mesma folha
que o editor renderiza, no estilo do Notion, e não um <input> separado com borda
ou fundo próprios. Ele é estado local simples, nunca um nó dentro do
documento do Tiptap: nunca aparece em document, onChange nem no
json/html de getSaveData(), só no próprio campo title.
initialTitledefine o título uma única vez, do mesmo jeito quedocumentdefine o conteúdo: mudar o valor depois não atualiza um editor já montado. Remonte com umakeydo React ao trocar de nota, como nodocument(veja Trocando de nota).titlePlaceholder(padrão"Título") só aparece enquanto o título está vazio, no estilo apagado de placeholder, e some assim que o usuário digita. Ele nunca faz parte do valor salvo (title === ""enquanto aparece, nunca o próprio texto do placeholder).- Enter no título nunca insere quebra de linha e nunca transforma o título
em heading: o título continua em uma linha e o foco vai para o
primeiro bloco do conteúdo, igual a apertar "seta para baixo" saindo do título de uma
página do Notion. Backspace bem no início do conteúdo faz o
contrário: devolve o foco para o fim do título em vez de não fazer
nada. Antes disso, o editor usa o Backspace quando tem o que fazer com ele:
logo depois de um atalho como
-ou#ele desfaz o atalho, no início de uma lista ou citação ele tira o bloco dela e num título de seção vazio ele volta a ser parágrafo. O Backspace seguinte vai para o título. autofocusé a mesma prop de antes; nenhuma prop nova foi criada para isso. Quando ainda não háinitialTitle(uma nota nova), oautofocusfoca primeiro o título; quando há (uma nota existente sendo aberta), foca o conteúdo, como já acontecia.showTitle(padrãotrue) esconde a área de título por completo quando éfalse. Serve para um host cujodocumentjá abre com o próprio heading fazendo o papel de título, para a barra de título separada não duplicar isso.initialTitle/titlePlaceholderegetSaveData().titlecontinuam funcionando por baixo de qualquer jeito; isso só esconde a UI. Com ela desligada, oautofocusnunca vai para o título (não há nada para focar) e o Backspace bem no início do conteúdo não passa mais o foco para um campo de título: ele segue o comportamento normal do editor. As visualizações acima (nesta seção "Título") usam o caso padrão,showTitle={true}; a visualização "Básico" mais acima usashowTitle={false}, porque o documento de exemplo já abre com o próprio heading "Meu título 01".
Uso na web (@medway-ui/core/note-editor)
Roda o Tiptap direto no DOM, sem WebView. Use em qualquer
app React que já renderiza @medway-ui/core.
import { useRef } from "react";
import {
NoteEditor,
type EditorDocument,
type NoteEditorHandle,
} from "@medway-ui/core/note-editor";
function Notes({ initialDoc }: { initialDoc: EditorDocument }) {
const ref = useRef<NoteEditorHandle>(null);
return (
<NoteEditor
ref={ref}
document={initialDoc}
autofocus="end"
onChange={(doc) => {
// doc: EditorDocument. Persista (ex.: debounce + salvar na sua API)
}}
/>
);
}
// Acesso imperativo quando o host tem a própria toolbar ou o próprio slash menu:
// ref.current?.focus("end");
// ref.current?.blur();
// ref.current?.getDocument();
// ref.current?.getSaveData(); // { title, json, html, removedImages }; veja "Salvando" abaixo
// ref.current?.setDocument(nextDoc, { focus: true });
// ref.current?.editor // instância `Editor` do Tiptap por baixoO NoteEditor é exportado pelo subpath @medway-ui/core/note-editor, não pela
raiz do pacote. É isso que torna o Tiptap uma peer dependency de fato opcional:
o barrel da raiz não importa nada dele, então um app que só renderiza Button nunca
precisa instalá-lo. Para usar o editor, instale os pacotes do Tiptap:
npm install @tiptap/react @tiptap/core @tiptap/pm \
@tiptap/suggestion @tiptap/extensions @tiptap/extension-document \
@tiptap/extension-paragraph @tiptap/extension-text @tiptap/extension-heading \
@tiptap/extension-bold @tiptap/extension-italic @tiptap/extension-underline \
@tiptap/extension-strike @tiptap/extension-code @tiptap/extension-code-block \
@tiptap/extension-blockquote @tiptap/extension-horizontal-rule \
@tiptap/extension-hard-break @tiptap/extension-link @tiptap/extension-listO @medway-ui/notes-core, que define o documento e o schema, é dependência do
@medway-ui/core desde a versão 1.36.0 e é instalado junto com ele. O tipo do
documento sai da mesma entrada do editor:
import type { EditorDocument } from "@medway-ui/core/note-editor". Só declare o
@medway-ui/notes-core no seu package.json se usar funções dele (como
normalizeDocument ou EMPTY_DOCUMENT), e na mesma faixa que o core usa
(^0.2.1), nunca >=: com >=, uma versão nova do notes-core entraria no app
sem entrar no core, e os dois leriam o documento com regras diferentes. Em versões
do core anteriores à 1.36.0, instale também o @medway-ui/notes-core.
Imagens
O NoteEditor nunca fala com um backend: ele não sabe como são o R2, o S3 ou a
sua própria API. O suporte a imagens é só um par de callbacks; você decide o que
acontece de fato com o arquivo:
<NoteEditor
ref={ref}
document={initialDoc}
toolbar
onImageUpload={async (file) => {
// Faça o upload para onde o seu app guarda arquivos e resolva com a URL que
// vai parar no JSON do documento. Ex.: um PUT pré-assinado no R2/S3 e depois a
// URL pública resultante.
return uploadToR2(file);
}}
onImageRemoved={(src) => {
// Upload que nunca entrou na nota: URL recusada pelo editor ou upload que
// terminou depois de o editor fechar. Pode apagar na hora.
deleteFromR2(src);
}}
/>const pendingRemovals = useRef<string[]>([]);
async function save() {
const { removedImages, ...note } = ref.current!.getSaveData();
await api.saveNote(note);
// Só depois de salvar com sucesso. Substitua a lista, não acumule.
pendingRemovals.current = removedImages;
}
// Ao fechar o editor (desmontar ou trocar de nota com `key`).
useEffect(() => () => pendingRemovals.current.forEach(deleteFromR2), []);Passar onImageUpload é o que ativa o suporte a imagens. Sem ele, a
toolbar não tem o botão "Inserir imagem" (o ícone de imagem), o menu / não tem a opção "Imagem", e soltar
ou colar um arquivo de imagem no editor é ignorado em silêncio. Então um host que
não quer imagens só omite a prop e não recebe nada dessa UI.
Imagens que vêm dentro de HTML colado ou solto (de outra página, de um e-mail)
ficam de fora, com ou sem a prop: elas entrariam como link para o servidor de
outra pessoa ou como base64 dentro do JSON, sem passar pelo onImageUpload. O
texto em volta entra normalmente. Copiar, recortar e colar uma imagem dentro da
própria nota continua funcionando; uma imagem copiada de outra nota fica de fora.
Com ela, quatro caminhos passam por ele: o botão "Inserir imagem" da toolbar
(precisa de toolbar também), o slash command /Imagem, soltar um arquivo de imagem
no editor e colar uma imagem da área de transferência. Cada um só insere o
nó de imagem depois que a promise do seu onImageUpload resolve com uma URL; se ela
rejeitar, o editor registra o erro no console e não insere nada. A imagem entra
onde o envio começou (no cursor ou no ponto em que foi solta), mesmo que o texto
mude enquanto o arquivo sobe, e sem texto alternativo: o nome do arquivo
(IMG_2034.JPG) não descreve a imagem para quem usa leitor de tela.
Imagens removidas durante a edição saem em getSaveData().removedImages.
O NoteEditor registra toda URL que voltou do onImageUpload nesta sessão, e
removedImages lista as que não estão no json daquele save:
- Apague só o que um save que deu certo deixou de fora, e só quando o editor fechar. Guarde a lista do último save com sucesso (substitua a cada save, não acumule) e apague os arquivos quando o editor desmontar ou trocar de nota. São dois motivos. Se o save falhar ou for cancelado (um autosave com debounce cancelado ao sair da tela, por exemplo), a nota salva ainda usa a imagem. E enquanto o editor está aberto, Ctrl/Cmd+Z pode trazer de volta uma imagem removida: se o arquivo já tivesse sido apagado, ela voltaria apontando para um arquivo que não existe mais.
onImageRemovedsó dispara na hora, nas três situações em que a imagem nunca chegou ao documento: quando oonImageUploadresolve com uma URL que o editor recusa (tudo que não éhttp(s):oudata:absoluto), quando o upload termina depois de a edição ser desligada e quando ele termina depois de o editor desmontar. Imagens removidas durante a edição nunca passam por ele.setDocumentesquece as imagens enviadas antes dele, porque o conteúdo anterior ainda pode fazer referência a elas.- Imagens que já estavam na prop
documentquando o editor montou, ou seja, carregadas de uma nota existente e já salva, nunca entram emremovedImages. O seu endpoint de salvar é o único lugar que conhece o documento antigo e o novo, então deixe que ele compare e apague por lá. - O editor não enxerga as outras notas. Se a pessoa copia ou recorta uma
imagem enviada nesta nota e cola em outra, ela pode aparecer no
removedImagesdesta nota e continuar em uso na outra. Se o seu app permite isso, confira no backend que nenhuma nota usa o arquivo antes de apagar. - É uma limpeza de melhor esforço. Uma imagem enviada depois do último save que deu certo, com o editor fechado sem salvar de novo, fica no storage sem nada apontando para ela. Se arquivos órfãos importam para você, combine com uma varredura periódica no backend.
| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
| onImageUpload | (file: File) => Promise<string> | - | Faz o upload do arquivo para onde o seu app guarda imagens e resolve com a URL a ser gravada no documento. É ela que ativa o botão "Inserir imagem" da toolbar, o slash command /Imagem, o arrastar e soltar e o colar. Sem ela, a inserção de imagens fica desativada em todos os lugares. |
| onImageRemoved | (src: string) => void | - | Função chamada na hora para um upload que nunca entrou na nota: URL recusada pelo editor ou upload que terminou depois de a edição ser desligada ou de o editor desmontar. Imagens removidas durante a edição vêm em getSaveData().removedImages. |
| imageDeleteGraceMs | number | - | Obsoleta e ignorada. As imagens removidas agora vêm em getSaveData().removedImages, e o host apaga quando o editor fecha. |
Redimensionando
Selecionar uma imagem (clicando nela ou chegando nela pelas setas) mostra uma
barra flutuante com tamanhos prontos (25%, 50%, 75%, 100% e
Original) e uma alça em cada canto para arrastar até qualquer tamanho
entre eles. Um arraste inteiro vira um único passo de desfazer, e escolher o
tamanho que a imagem já tem não mexe no documento (sem passo de desfazer, sem
onChange).
Pelo teclado: com a imagem selecionada, Tab leva o foco para a barra; ←/→ passam entre os tamanhos, Enter aplica um, Esc ou Shift+Tab voltam para a imagem e Tab segue para o texto depois dela.
O tamanho fica no nó da imagem como attrs.width: uma porcentagem da
largura do bloco em que a imagem está (a coluna de conteúdo do editor), um
inteiro de MIN_IMAGE_WIDTH (10) a MAX_IMAGE_WIDTH (100). Sem width (ou
com null), a imagem fica no tamanho natural, limitado à largura da coluna,
como todas as imagens criadas antes deste recurso.
{ "type": "image", "attrs": { "src": "https://…/a.png", "width": 50 } }Como o tamanho é relativo, a mesma nota mantém as proporções em qualquer tela. Numa tela estreita, uma porcentagem pequena ficaria minúscula (25% de um celular de 360px dá ~90px), então uma imagem redimensionada nunca fica mais estreita que 120px, ou que a coluna, se ela for ainda mais estreita.
normalizeDocument (e sanitizeImageWidth, exportado pelo
@medway-ui/notes-core junto com as duas constantes) só mantém width quando
ele é um número ou uma string decimal simples, arredonda e limita entre 10 e
100; qualquer outro valor é descartado, então um documento editado à mão
continua abrindo.
Saída em HTML. Quando a nota é salva ou renderizada como HTML (veja Salvando), uma imagem redimensionada sai assim:
<img src="…" data-width="50" style="width: 50%; min-width: min(100%, 120px)">Se o seu host sanitiza esse HTML, mantenha o atributo data-width e as
propriedades width / min-width do style na img, senão as imagens
redimensionadas voltam ao tamanho natural. Ao ler o HTML de volta, o editor
procura primeiro o data-width e, se não houver, um style="width: N%".
Versão necessária. O redimensionamento precisa do @medway-ui/notes-core
0.2 ou mais novo, na web e no mobile (o bundle do editor e o normalizeDocument
do @medway-ui/native também precisam manter o width). Um notes-core mais
antigo descarta o atributo quando o documento é normalizado. A partir do
@medway-ui/core 1.36.0 e do @medway-ui/native 1.9.0 o notes-core vem junto,
na versão certa; em versões anteriores, atualize os pacotes juntos.
Salvando: título, JSON e HTML
A fonte da verdade do documento é sempre JSON (EditorDocument). É o que o
@medway-ui/native também lê pela ponte do WebView (veja abaixo), então uma nota
criada em uma plataforma abre na outra sem conversão. Mas um campo do backend
que já guarda HTML não precisa travar este editor até ser
migrado: o próprio NoteEditor lê e escreve os dois formatos. O
título fica separado dos dois (veja Título) e vai junto
deles como um campo próprio.
import { useRef } from "react";
import { Button } from "@medway-ui/core";
import {
NoteEditor,
type EditorDocument,
type NoteEditorHandle,
} from "@medway-ui/core/note-editor";
function Notes({ initialNote }: { initialNote: { title: string; content: EditorDocument | string } }) {
const ref = useRef<NoteEditorHandle>(null);
const handleSave = async () => {
const saveData = ref.current?.getSaveData();
if (!saveData) return;
const { title, json, html } = saveData;
// `title` nunca aparece em `json`/`html`; envie como um campo próprio.
await NotesAPI.save({ title, content: html, content_json: json });
};
return (
<>
{/* `initialNote.content` pode ser um EditorDocument ou uma string HTML;
os dois passam pelo mesmo schema que o editor renderiza. */}
<NoteEditor
ref={ref}
initialTitle={initialNote.title}
document={initialNote.content}
toolbar
/>
<Button onClick={handleSave}>Salvar</Button>
</>
);
}document(prop) esetDocument(ref) aceitamEditorDocument | string. Uma string é tratada como HTML e passa pelo schema de notas (generateJSON) antes de ser filtrada pela mesma whitelist que o JSON já passa (normalizeDocument). O HTML de um cliente mais antigo segue exatamente o mesmo caminho de "entrada não confiável" que qualquer outro documento, sem confiança cega.- A saída passa pela mesma whitelist. O JSON de
onChange,getDocumentegetSaveData(e ohtmlgerado a partir dele) já sai donormalizeDocument. Um link colado de outra página guarda só ohref:class,title,targetereldo HTML de origem nunca entram na nota. getSaveData()calcula tudo de uma vez ({ title, json, html, removedImages }) e foi feito para ser chamado pela sua própria ação de salvar (um clique de botão, um timer de debounce), nunca peloonChange. OonChangecontinua entregando só o JSON do conteúdo, a cada tecla, e nunca o título; rodargenerateHTMLcom essa frequência seria trabalho jogado fora em edições que nunca são salvas.titlenunca faz parte dejsonnem dehtml. Digitar "Arquitetura do projeto" no título e "Hoje vamos estudar cache distribuído." no conteúdo salva como{ title: "Arquitetura do projeto", json: {"type": "doc", "content": [{"type": "paragraph", ...}]}, html: "<p>Hoje vamos estudar cache distribuído.</p>" }, nunca um<h1>dentro dehtmlou um nóheadingdentro dejson.- O JSON continua sendo o formato canônico. Envie HTML enquanto o campo do backend precisar
dele, mas continue persistindo o JSON junto (mesmo numa coluna sem uso):
é o único formato que todo cliente (incluindo o
@medway-ui/native) consegue ler sem perder informação numa ida e volta pelo schema mais restrito do HTML. - O tipo de retorno de
getSaveDataé exportado comoNoteEditorValue:import type { NoteEditorValue } from "@medway-ui/core/note-editor". O nome antigoNoteEditorSaveData(de antes de existirtitle) continua como alias dele, então imports existentes e a desestruturação{ json, html }continuam compilando sem mudança.
Trocando de nota
setDocument substitui o conteúdo de um editor em execução: a instância do
ProseMirror, e com ela o histórico de desfazer, continua viva. Esse é o
comportamento certo para substituir o conteúdo da nota que o usuário já está editando
(aplicar um template, recarregar depois de um conflito), mas é o errado para
abrir uma nota diferente: Ctrl/Cmd+Z voltaria
para o conteúdo da nota anterior e depois o salvaria por cima da nova.
Ele também não mexe no título, que é um estado separado.
Para trocar de nota, remonte o componente, nas duas plataformas:
<NoteEditor
key={note.id}
initialTitle={note.title}
document={note.content}
/>Mudar a key dá à nova nota uma instância nova: pilha de desfazer vazia, o próprio
initialTitle e o próprio controle de imagens. setDocument serve para todo o
resto.
O mobile se comporta do mesmo jeito e precisa da mesma
key: osetDocumentdo@medway-ui/nativeenviasetContentcomreset: false. O caminhoreset: truedo protocolo recria o editor do lado do bundle, mas só roda quando oNoteEditormonta; ele não é acessível pela ref pública.
Os textos embutidos são em pt-BR e não são configuráveis
Todo texto que o próprio editor renderiza está fixo em português do Brasil.
Não existe prop locale/labels, em nenhuma das plataformas:
- os rótulos da toolbar (
aria-labele item do menu "Mais opções"): "Negrito", "Título 2", "Desfazer", "Inserir imagem", e o próprio "Mais opções"; - as dicas (tooltips) da toolbar, que somam o atalho ao rótulo: "Negrito
(Ctrl/Cmd+B)". O atalho também vai em
aria-keyshortcuts; - o aviso de upload "enviando imagem…";
- os
aria-labels do próprio editor: "Conteúdo da anotação", "Formatação", "Título da anotação", "Comandos"; - o nome do checkbox de cada item da checklist: "Marcar tarefa: …" ou, já marcado, "Desmarcar tarefa: …", com o texto do item ("tarefa vazia" sem texto);
- as opções do menu
/e suas descrições ("Título 1" / "Título de seção grande", "Checklist" / "Lista de tarefas", …) e o estado vazio "Nenhum resultado"; - o dialog de link (na web, "Adicionar link"/"Editar link" com os campos
"Texto" e "Link", aberto pela barra ou por Ctrl/Cmd+K; no mobile, o dialog
"Inserir link") e o erro "Link inválido. Use um endereço como medway.com.br".
Nas duas plataformas o campo aceita
medway.com.br, um e-mail ou a URL completa (normalizeLinkInput, do@medway-ui/notes-core), e sem texto selecionado a própria URL entra como texto do link; - os placeholders do conteúdo:
Comece a escrever ou digite "/" para comandoseDigite "/" para comandos.
Só o titlePlaceholder é prop. Os placeholders do conteúdo são opções do
buildExtensions (placeholder, caretHint) uma camada abaixo, em
@medway-ui/notes-core, mas o NoteEditor não os repassa. Um host que
precisa de outros textos hoje tem que montar o próprio editor a partir de
@medway-ui/notes-core/schema em vez de configurar este.
Uso no mobile (@medway-ui/native/note-editor)
O React Native não tem DOM, então o mesmo schema roda dentro de um WebView
travado (navegação bloqueada, file:// desligado, sem novas janelas). O conteúdo
continua atravessando a ponte como JSON (o próprio documento, nunca HTML), mas desde
a versão 3 do protocolo essa ponte é totalmente interna: o NoteEditor aqui recebe
as mesmas props declarativas do NoteEditor da web (document, onChange,
editable, autofocus, toolbar, onImageUpload, onImageRemoved e a
obsoleta imageDeleteGraceMs) e controla sozinho o WebView e a ponte de mensagens. O host
não precisa mais ligar onMessage/injectJavaScript na mão.
import { useRef } from "react";
import {
NoteEditor,
type EditorDocument,
type NoteEditorHandle,
} from "@medway-ui/native/note-editor";
function NotesScreen({ initialDoc }: { initialDoc: EditorDocument }) {
const ref = useRef<NoteEditorHandle>(null);
return (
<NoteEditor
ref={ref}
document={initialDoc}
autofocus="end"
onChange={(doc) => {
// doc: EditorDocument. Persista (ex.: debounce + salvar na sua API)
}}
/>
);
}
// Acesso imperativo, com os mesmos nomes da ref da web. Aqui é assíncrono, porque ler
// o documento é uma ida e volta pela ponte nesta plataforma:
// ref.current?.focus("end");
// ref.current?.blur();
// await ref.current?.getDocument();
// await ref.current?.getSaveData(); // { title, json, removedImages }, sem `html`; veja abaixo
// ref.current?.setDocument(nextDoc, { focus: true });O tema também é automático: o NoteEditor lê os tokens do design system para o
esquema de cores atual (useThemeColor/useColorScheme do NativeWind) e
mantém o documento do WebView sincronizado. O host nunca precisa montar nem enviar um
EditorTheme por conta própria.
Diferenças em relação à web e por quê
Três props só existem por causa da plataforma, do mesmo jeito que
...props (atributos de div repassados) só faz sentido na web:
| Propriedade | Por que só existe no mobile |
|---|---|
| height | Altura inicial fixa, em dp, para o modo embutido (o editor dentro de um scroll container nativo, redimensionado conforme o conteúdo que ele informa pela ponte). Na web, a superfície só cresce junto com o bloco que a contém. |
| backgroundColor | Evita um flash antes de o WebView pintar o fundo do próprio documento (sensível no Android). Na web, a superfície só herda o fundo da página. |
| onLinkPress | O WebView nunca navega; um link tocado precisa ser entregue ao host (ex.: Linking.openURL, depois de validar a URL), como sempre foi nesta plataforma. |
Outras duas props existem nas duas plataformas, mas não puderam manter o mesmo formato, porque aquilo a que elas se adaptam é hardware/runtime de fato diferente, não só uma escolha de nome:
| Propriedade | Formato na web | Formato no mobile | Por quê |
|---|---|---|---|
| onImageUpload | (file: File) => Promise<string> | () => Promise<{ src: string; alt?: string | null } | null> | Na web, o <input type="file"> do navegador já escolheu um arquivo quando isto é chamado; só falta o host fazer o upload. Nesta plataforma não existe input de arquivo do DOM e o @medway-ui/native não tem dependência própria de seletor de imagens, então escolher a imagem (com o seletor que o app host já usa) também é papel desta função, não só o upload. Retorne null se o seletor do host foi cancelado. |
| getSaveData (ref) | () => NoteEditorValue, síncrono, { title, json, html, removedImages } | () => Promise<{ title, json, removedImages }>, assíncrono, sem html | Gerar html exige um DOM; esta plataforma de propósito nunca carrega um fora do WebView, e ler o documento já é uma ida e volta pela ponte, não uma chamada no mesmo processo. Persista o json; gere o HTML no backend se precisar. |
Todo o resto (document/setDocument (JSON ou string HTML, que passa
pelo schema de notas nos dois casos, só que aqui do lado do bundle, porque é o
único lado com um DOM para interpretar HTML), initialTitle, titlePlaceholder,
onChange, editable, autofocus, toolbar, onImageRemoved, a obsoleta
imageDeleteGraceMs e os métodos focus/blur/setDocument da ref) é a mesma
prop, com o mesmo tipo e o mesmo comportamento do NoteEditor da web. Veja
Título, Imagens e
Salvando acima; tudo o que está documentado lá
vale aqui também.
Como na web, o NoteEditor é exportado por um subpath, @medway-ui/native/note-editor,
e não pela raiz do pacote. O barrel da raiz não importa react-native-webview
nem o HTML embutido do editor, então um app que só renderiza Button não paga
por nenhum dos dois. Isso importa mais aqui do que na web: o Metro não faz tree-shaking
de forma confiável, então tudo o que é alcançável pelo barrel vai parar no bundle do app.
Os tipos de mensagem ficam sozinhos em @medway-ui/native/editor/protocol.
Instalação:
npm install @medway-ui/native react-native-webviewComo na web, o @medway-ui/notes-core vem junto com o @medway-ui/native (desde a
1.9.0), e o tipo EditorDocument sai de @medway-ui/native/note-editor. Declare o
notes-core no app só se usar funções dele, em ^0.2.1.
Veja o README do @medway-ui/native
para a configuração de Metro/Babel/NativeWind que este pacote precisa num app brownfield.
API
Web: props do NoteEditor (@medway-ui/core/note-editor)
| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
| document | EditorDocument | string | - | Conteúdo inicial: JSON ou uma string HTML (que passa pelo schema de notas). Só é lido uma vez; use o setDocument da ref para substituir o conteúdo depois. Veja Salvando. |
| initialTitle | string | - | Título inicial, exibido acima do conteúdo. É um estado próprio, nunca um nó em document/json/html. Só é lido uma vez, como o document. Veja Título. |
| titlePlaceholder | string | "Título" | Exibido enquanto o título está vazio. Nunca faz parte do valor salvo. Veja Título. |
| showTitle | boolean | true | Define se a área de título é renderizada. Use false quando o document já abre com o próprio heading fazendo o papel de título. Veja Título. |
| onChange | (doc: EditorDocument) => void | - | Dispara a cada atualização do Tiptap. Só para o conteúdo; nunca dispara em mudanças do título. |
| editable | boolean | true | Somente leitura quando false. Vale também para a área de título. |
| autofocus | boolean | "start" | "end" | false | Foca na montagem: o título se initialTitle estiver vazio (uma nota nova), senão o editor. Veja Título. |
| extraExtensions | Tiptap Extension[] | - | Extensões só de UX, somadas ao schema de notas (floating menu, menção etc.). O slash menu / já vem embutido e não precisa de extensão. |
| toolbar | boolean | false | Mostra uma barra de formatação acima do editor, em cinco grupos: marcas (negrito, itálico, sublinhado, tachado, código inline), headings (H2, H3), blocos (lista com marcadores, lista numerada, citação, bloco de código), link e histórico (desfazer, refazer). Um sexto grupo, com o botão "Inserir imagem" (o ícone de imagem), entra quando onImageUpload está definido; veja Imagens. Cada botão tem uma dica com o nome e, quando houver, o atalho. Quando a largura não comporta todos os botões, os do fim da barra vão, na mesma ordem, para um menu Mais opções (⋮); a conta usa o tamanho real dos botões, então acompanha a fonte do navegador. No menu, os comandos de alternância (negrito, títulos, listas…) aparecem marcados quando ativos. É independente do menu /: use um, os dois ou nenhum. |
| onImageUpload | (file: File) => Promise<string> | - | Ativa o suporte a imagens (botão da toolbar, /Imagem, arrastar e soltar, colar). Veja Imagens. |
| onImageRemoved | (src: string) => void | - | Função chamada na hora para um upload que nunca entrou na nota (URL recusada ou upload que terminou tarde). Imagens removidas durante a edição vêm em getSaveData().removedImages. Veja Imagens. |
| imageDeleteGraceMs | number | - | Obsoleta e ignorada. Use getSaveData().removedImages. Veja Imagens. |
| ...props | React.ComponentProps<"div"> | - | Repassadas para a div que envolve o editor (className etc.). |
Web: NoteEditorHandle (ref)
| Propriedade | Tipo | Descrição |
|---|---|---|
| editor | Editor | null | Instância do Tiptap por baixo, para uma toolbar ou atalhos personalizados. É null até o editor ser criado: no Next.js o @tiptap/react só cria o editor depois da primeira renderização. Nesse intervalo, getDocument e getSaveData já devolvem a nota inicial e um setDocument é aplicado assim que o editor existe. |
| focus / blur | (position?) => void / () => void | - |
| getDocument | () => EditorDocument | Lê o conteúdo atual como JSON. |
| getSaveData | () => NoteEditorValue: { title: string; json: EditorDocument; html: string; removedImages: string[] } | Calcula o título, os dois formatos de conteúdo e as imagens enviadas nesta sessão que saíram do documento (veja Imagens), para a sua ação de salvar. Não dispara a cada tecla como o onChange; chame só quando for de fato persistir. Veja Salvando. |
| setDocument | (doc: EditorDocument | string, options?: { focus?: boolean }) => void | Substitui o conteúdo na mesma instância. Também aceita uma string HTML; veja Salvando. Três coisas que ele de propósito não faz: nunca dispara onChange (a substituição não é uma edição do usuário, então um host que marca a nota como "suja" pelo onChange continua limpo), nunca mexe no título (não existe setter imperativo de título) e nunca limpa o histórico de desfazer. Para trocar de nota, remonte com uma key do React; veja Trocando de nota. |
Mobile: props do NoteEditor (@medway-ui/native/note-editor)
As mesmas props da web (document, initialTitle, titlePlaceholder, onChange,
editable, autofocus, toolbar, onImageRemoved e a obsoleta imageDeleteGraceMs) mais
as específicas da plataforma abaixo. Veja
Diferenças em relação à web e por quê para
o formato adaptado do onImageUpload.
| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
| onImageUpload | () => Promise<{ src: string; alt?: string | null } | null> | - | Escolhe (com o seletor do próprio host) e faz o upload de uma imagem; resolva com null se o seletor do host foi cancelado. Ativa o botão "Inserir imagem" da toolbar e o slash command /Imagem. Como na web, sem ela os dois ficam indisponíveis. |
| onLinkPress | (href: string) => void | - | Só no nativo. O WebView nunca navega; um link tocado é entregue aqui (ex.: Linking.openURL depois de validar a URL). |
| height | number | - | Só no nativo. Altura inicial fixa para o modo embutido (dentro de um scroll container nativo), redimensionada conforme o conteúdo cresce ou diminui. Omita para o WebView preencher o pai e rolar dentro dele. |
| backgroundColor | string | - | Só no nativo. Use a cor do tema do host para evitar um flash antes de o documento ser pintado (sensível no Android). |
| testID | string | - | - |
Mobile: NoteEditorHandle (ref)
Os mesmos nomes de métodos da ref da web. getDocument/getSaveData são async
aqui (ler o documento é uma ida e volta pela ponte nesta plataforma, não uma
chamada no mesmo processo) e não existe o campo editor (não há instância do Tiptap
no mesmo processo para entregar, só um WebView).
| Propriedade | Tipo | Descrição |
|---|---|---|
| focus / blur | (position?) => void / () => void | - |
| getDocument | () => Promise<EditorDocument> | Lê o conteúdo atual como JSON, pela ponte. |
| getSaveData | () => Promise<{ title: string; json: EditorDocument; removedImages: string[] }> | Sem html; veja Diferenças em relação à web e por quê. |
| setDocument | (doc: EditorDocument | string, options?: { focus?: boolean }) => void | Substitui o conteúdo na mesma instância. Também aceita uma string HTML, interpretada do lado do bundle. Mesmas ressalvas da web (sem onChange, título intocado, histórico de desfazer preservado), então trocar de nota aqui também exige uma key do React. Veja Trocando de nota. |
Avançado: a ponte do WebView (@medway-ui/native/editor/protocol)
Tudo o que está acima é o que um host típico precisa. O protocol.ts (tipos de mensagem,
EDITOR_PROTOCOL_VERSION, EDITOR_RECEIVE_HANDLER, isEditorMessage) continua
exportado, mas desde a versão 3 do protocolo é um detalhe de implementação
que o NoteEditor controla internamente. Use só se estiver construindo uma
superfície personalizada direto sobre o WebView em vez de usar o NoteEditor,
ou mexendo no próprio editor-bundle/NoteEditor.tsx. O EDITOR_PROTOCOL_VERSION
continua protegendo contra um bundle desatualizado: ele sobe sempre que o formato de uma mensagem
muda, e o bundle se recusa a conversar com um lado nativo que informa uma
versão diferente.