Elementos
RichEditor
RichEditor é um editor de rich text construído segundo a arquitetura do ProseMirror. O app define de que um documento é feito com um schema (parágrafos, títulos, listas, menções, quaisquer tipos de nodes e marks de que precise), e o editor mantém todo documento válido em relação a ele. As alterações são steps agrupados em transactions, de modo que podem ser desfeitas, mapear posições e trafegar como JSON; o comportamento vem de plugins (histórico, keymaps, input rules, atalhos de Markdown, os do próprio app); commands são valores que uma barra de ferramentas executa e consulta. O estado do editor, seus commands e seus plugins rodam em Rust: digitação, atalhos de teclado e input rules nunca esperam pelo JS, e o app lê o estado como JSON quando o solicita.
import { mount, signal, Text, View } from "nsh";
import { RichEditor, StarterKit, command, createRichEditor, redo, undo } from "nsh/base";
import { button, buttonText, desktop, Log, Panel, Split, ui, wide } from "./palette";
const CONTENT = `# A document
Type **Markdown** as you go: \`#\` makes a heading, \`-\` a list, \`[ ]\` a task, each followed by a space.
- Select text and press Bold
- Undo and redo keep every step
`;
// The session owns the document; the view renders it and turns input into
// transactions. The toolbar runs Tiptap's named commands on the session.
function App() {
const editor = createRichEditor({ extensions: [StarterKit], content: CONTENT, format: "markdown" });
const markdown = signal(editor.getMarkdown());
const events = signal<string[]>([]);
const log = (line: string) => events.set([line, ...events()].slice(0, 4));
editor.on("revision", () => markdown.set(editor.getMarkdown()));
editor.on("focus", () => log("focus"));
editor.on("blur", () => log("blur"));
const action = (label: string, run: () => void) => (
<View onPress={run} role="button" accessibilityLabel={label} {...button("secondary", { size: "sm" })}>
<Text style={buttonText()}>{label}</Text>
</View>
);
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>rich editor</Text>
<Text style={{ color: ui().muted }}>
{desktop ? "Click into the document and type; Cmd/Ctrl+B makes bold" : "Tap into the document and type"}
</Text>
<Split
grow
width={640}
inspector={
<>
<Panel title="markdown">
<Text style={{ fontSize: 12, fontFamily: "monospace", color: ui().text }}>{markdown()}</Text>
</Panel>
<Log title="events" lines={events()} minHeight={72} />
</>
}
>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
{action("Bold", () => editor.run(command("toggleBold")))}
{action("Italic", () => editor.run(command("toggleItalic")))}
{action("Heading", () => editor.run(command("toggleHeading", { level: 2 })))}
{action("List", () => editor.run(command("toggleBulletList")))}
{action("Task", () => editor.run(command("toggleTaskList")))}
{action("Undo", () => editor.run(undo))}
{action("Redo", () => editor.run(redo))}
</View>
<View style={{ height: wide() ? 360 : 300, ...ui().card, borderRadius: 14, overflow: "hidden" }}>
<RichEditor
editor={editor}
accessibilityLabel="Document"
placeholder="Write something"
classes={CLASSES}
style={{ flex: 1 }}
/>
</View>
</Split>
</View>
);
}
/** The document's look: NSH's editor draws nothing of its own. */
const CLASSES = {
content: "p-4 gap-3",
taskCheckbox: "size-4 mr-2 mt-0.5 rounded border border-border checked:bg-primary checked:border-primary",
listMarker: "text-muted-foreground",
nodes: {
"heading[level=1]": "text-2xl font-bold",
"heading[level=2]": "text-xl font-semibold",
"heading[level=3]": "text-lg font-semibold",
blockquote: "border-l-4 border-border pl-3 text-muted-foreground",
codeBlock: "font-mono text-sm bg-muted rounded-md px-3 py-2",
bulletList: "gap-1 pl-5",
orderedList: "gap-1 pl-5",
taskList: "gap-1",
taskItem: "flex-row",
horizontalRule: "h-px bg-border my-2",
},
marks: {
bold: "font-semibold",
italic: "italic",
strike: "line-through",
code: "font-mono text-sm bg-muted rounded px-1",
link: "text-primary underline",
},
};
mount(App);Um editor tem duas partes: a sessão (createRichEditor), que detém o estado e é conduzida pelo código do app, e a view (<RichEditor editor={…} />), que renderiza a sessão de forma nativa e converte a entrada da plataforma em transactions. Várias views podem mostrar uma mesma sessão.
O kernel do editor é um port em Rust do ProseMirror, de Marijn Haverbeke, e sua camada de extensões segue o Tiptap: suas estruturas de dados, commands e nomes foram mantidos, e o que você já sabe sobre eles continua valendo. Ambos são licenciados sob MIT, e suas licenças se aplicam ao que foi portado deles.
import { mount } from "nsh";
import { RichEditor, createRichEditor, history, inputRules, keymap, markdown, markdownRules, starterKeymap, baseKeymap, toggleMark } from "nsh/base";
const editor = createRichEditor({
content: "# Notes\n\nStart typing…",
format: "markdown",
plugins: [
history(),
keymap({ "Mod-b": toggleMark("bold"), "Mod-i": toggleMark("italic") }),
starterKeymap(),
baseKeymap(),
inputRules(markdownRules()),
markdown(),
],
});
function App() {
return <RichEditor editor={editor} class="flex-1" />;
}
mount(App);
Sem schema, um editor usa o schema inicial (starterSchema()); sem plugins, ele recebe o starter kit (starterKit(): as input rules e os atalhos de teclado do starter, os atalhos base e o histórico). Um array plugins vazio cria um editor sem nenhum plugin.
#Estilização
O editor desenha o documento com a aparência do app: classes e styles estilizam suas partes (root, content, caret, selection, placeholder, composition, selectedNode, listMarker e, para tabelas, selectedCell, tableGrip, tableResizeHandle, tableDropIndicator), seus nodes por tipo (nodes: { heading: "text-2xl font-bold", "heading[level=2]": "text-xl" }) and its marks (marks: { bold: "font-bold" }). Os itens de listas cujo node spec define extra.listMarker como "bullet" ou "ordered" (as listas do schema inicial) mostram um marcador na parte listMarker: •, ◦, ▪ conforme a profundidade, ou números a partir do start da lista, posicionado antes do item, no recuo da lista (pl-6 na lista, como um navegador recua ul).
<RichEditor
editor={editor}
classes={{
nodes: { heading: "text-2xl font-bold", bulletList: "pl-6", codeBlock: "font-mono bg-muted p-3 rounded-md" },
marks: { bold: "font-bold", italic: "italic", code: "font-mono bg-muted" },
listMarker: "text-muted-foreground",
}}
/>
A parte content é a caixa do documento dentro da scroll view do editor: seu padding (content: "px-6 py-5") é o espaço ao redor do texto, e a barra de rolagem corre pela própria borda do editor. Um arraste que seleciona além do topo ou da base do editor rola o documento junto, mais rápido quanto mais longe o ponteiro for.
#Bubble menu
bubbleMenu é o bubble menu do Tiptap: o conteúdo do app exibido ao lado da seleção, acima dela por padrão (bubbleMenuPlacement, passando para o outro lado onde não houver espaço) a bubbleMenuOffset pontos (8), em um overlay que acompanha o texto durante a rolagem. O app o passa enquanto quiser que seja exibido, a partir do estado da sessão:
const revision = signal(editor.revision);
editor.on("revision", ({ revision: r }) => revision.set(r));
const inLink = () => (revision(), editor.isActive("link"));
<RichEditor
editor={editor}
bubbleMenu={inLink() ? <LinkMenu href={editor.getAttributes("link").href} /> : undefined}
bubbleMenuPlacement="bottom"
/>
Um menu de link edita o link sob o cursor com chain([extendMarkRange("link"), command("setLink", { href })]) e o remove com command("unsetLink") após o mesmo extendMarkRange.
#Schemas
defineSchema(spec) lê um schema no formato do ProseMirror e lança um erro indicando o que está errado nele. Os tipos de node nomeiam seu content (uma content expression: "inline*", "paragraph+", "(heading | paragraph) block*"), seu group, se são inline ou um atom, os marks que permitem e seus attrs com valores padrão e validação ("string", "number", "boolean"). O primeiro tipo de bloco é o padrão: o que o Enter cria e o que preenche conteúdo obrigatório. Chaves que um spec não conhece são mantidas como dados extras do tipo, para frameworks construídos sobre o editor.
import { createRichEditor, defineSchema } from "nsh/base";
const schema = defineSchema({
nodes: {
doc: { content: "block+" },
paragraph: { content: "inline*", group: "block" },
mention: { group: "inline", inline: true, atom: true, attrs: { id: {}, label: { default: "" } } },
text: { group: "inline" },
},
marks: { bold: {}, link: { attrs: { href: {} }, inclusive: false } },
});
const editor = createRichEditor({ schema, doc: schema.parse("Hello") });
Um RichSchema lê e escreve documentos: parse(content, format?) (texto simples por padrão, "markdown" ou JSON), parseMarkdown(markdown), serialize(doc, format?) (Markdown por padrão, "text", "json") e check(doc), que retorna o documento com os valores padrão dos atributos preenchidos. nodes, marks e topNode nomeiam seus tipos. O Markdown é mapeado para os nomes do schema inicial (paragraph, heading, blockquote, codeBlock, bulletList, orderedList, listItem, bold, italic, code, link, strike).
#Commands
Um command é um valor: toggleMark("bold") cria um objeto que o app guarda, associa a teclas e passa ao editor. editor.run(command) o executa e retorna se ele foi aplicado; editor.can(command) consulta sem alterar nada, que é como uma barra de ferramentas habilita seus botões. editor.isActive(name, attrs?) informa se uma mark ou um node está ativo na seleção (isActive("heading", { level: 2 })).
| Command | O que faz |
|---|---|
toggleMark(name, attrs?) |
Adiciona a mark à seleção, ou a remove onde estiver presente; em um cursor, vale para o texto digitado em seguida |
setMark(name, attrs?) / unsetMark(name) |
Adiciona ou remove uma mark |
setBlockType(name, attrs?) |
Transforma os textblocks selecionados em um tipo (setBlockType("heading", { level: 2 })) |
wrapIn(name, attrs?) / lift() |
Envolve os blocos selecionados em um node (uma citação) / os retira de dentro do pai |
wrapInList(name, attrs?) |
Envolve os blocos selecionados em uma lista |
splitListItem(name) / sinkListItem(name) / liftListItem(name) |
Divide itens de lista, aumenta ou diminui seu recuo |
undo() / redo() |
Desfaz ou refaz uma alteração (com history()) |
selectAll(), setTextSelection(anchor, head?), setNodeSelection(pos) |
Seleciona |
insertText(text), insertContent(content, format?) |
Insere texto ou conteúdo sobre a seleção |
setNodeAttribute(pos, attr, value) |
Define um atributo do node em uma posição, como uma única alteração desfazível |
command(name) |
Um command sem parâmetros, por nome: "splitBlock", "joinBackward", "exitCode", "newlineInCode", "selectParentNode", … |
chain(commands) |
Executa commands um após o outro como uma única transaction (um passo de undo); só se aplica quando todos se aplicam |
first(commands) |
Executa o primeiro command que se aplica |
editor.run(chain([setBlockType("heading", { level: 1 }), toggleMark("bold")]));
const canIndent = editor.can(sinkListItem("listItem"));
#A sessão
| Membro | O que é |
|---|---|
state, doc, selection |
O estado, o documento e a seleção como JSON, gerados no momento da leitura |
revision |
Quantas transactions o estado aplicou |
isEmpty, undoDepth, redoDepth, composing |
O que uma UI mostra sobre o editor |
setContent(content, format?) |
Substitui o documento, como uma única alteração desfazível |
insertContent(content, format?) |
Insere conteúdo sobre a seleção |
getJSON(), getMarkdown(), getText(), textBetween(from?, to?, separator?) |
O documento em um formato |
dispatch(tr) |
Aplica uma transaction fornecida como JSON (seus steps sobre o documento atual) |
on(event, listener) |
Chama listener com o payload de cada event, no próximo turno do JS: revision ({ revision }, após cada alteração, de qualquer origem), update, selectionUpdate, transaction, focus, blur, delete. Retorna uma função que interrompe a escuta; um listener adicionado enquanto um componente renderiza é interrompido junto com o componente |
focus(), blur() |
Move o foco do teclado para as views que mostram o editor, ou para fora delas |
As posições são contadas como no ProseMirror: um texto conta suas unidades de código UTF-16, um node folha conta um, um node com conteúdo conta suas duas fronteiras. Tudo o que precisa sobreviver às edições guarda um id nos atributos de um node ou em uma mark. Uma consulta de posição (resolve(pos), nodeAt(pos), coordsAtPos(pos), findParentNode(name, pos), …) lança um RangeError para uma posição fora do documento (0 a doc.content.size), como faz o resolve do ProseMirror, e responde null onde não há nada (nodeAt no fim de um node, coordsAtPos sem nenhuma view mostrando o editor). run e can lançam um TypeError para um command, tipo de node ou mark que o editor não conhece.
#Assumindo o controle das transactions
editor.dispatchTransaction = (tr) => { … } faz o app confirmar cada transaction por conta própria: o handler recebe cada uma como JSON e a confirma com editor.applyTransaction(tr), ou a descarta. Colaboração, logging e transactions que precisam passar pelas verificações do app usam esse recurso; null devolve a confirmação ao editor. O handler ouve a digitação no próximo turno do JS, e o documento muda quando ele confirma.
#Extensions
As extensions são as do Tiptap: cada uma traz, em conjunto, seu tipo de node ou mark, seus atalhos de teclado, input e paste rules, plugins e commands nomeados, e um editor feito de extensions obtém delas seu schema, keymap, rules e tabela de commands. As embutidas são classes nomeadas como as do Tiptap, usadas como estão ou configuradas:
import { RichEditor, StarterKit, Underline, command, createRichEditor } from "nsh/base";
const editor = createRichEditor({
extensions: [StarterKit.configure({ heading: { levels: [1, 2, 3] }, codeBlock: false })],
content: "Hello",
editable: true,
});
editor.run(command("toggleHeading", { level: 2 }));
editor.run(command("toggleBold"));
StarterKit traz Document, Paragraph, o node de texto, Heading, Blockquote, BulletList, OrderedList, ListItem, TaskList, TaskItem, CodeBlock, HardBreak, HorizontalRule, RichImage (Image do Tiptap), Bold, Italic, Strike, Code, Underline, RichLink (Link do Tiptap), Highlight, Subscript, Superscript, RichTextStyle (TextStyle do Tiptap), TextAlign, History, TrailingNode, Typography e ListKeymap. Uma opção com o nome de uma delas definida como false a deixa de fora, e um objeto a configura (heading: { levels }, history: { depth }, textAlign: { types }, codeBlock: { enableTabIndentation }). O configure(options) e o extend({...}) de cada classe criam um RichExtension, que tem name, type, priority e options. Todo editor também recebe o keymap central (Enter, Backspace, Delete, selecionar tudo).
Cada extension adiciona os commands do Tiptap, que command(name, args) executa: toggleBold, setItalic, unsetStrike, toggleHeading({ level }), setParagraph, toggleBlockquote, toggleBulletList, toggleOrderedList, toggleTaskList, toggleCodeBlock({ language }), setHardBreak, setHorizontalRule, setImage({ src }), toggleHighlight({ color }), setTextAlign("center"), unsetTextAlign, além dos commands de link, de contagem de caracteres e de sugestão abaixo.
As alternâncias de lista atuam sobre listas inteiras, como os botões de lista de um processador de texto: toggleOrderedList em uma lista com marcadores a numera em todos os níveis, inclusive nas listas aninhadas, e o toggle próprio de uma lista retira os blocos selecionados de todas as listas ao redor.
A ordem de extensions é sua prioridade entre iguais: uma extension listada depois associa suas teclas antes das listadas antes dela; priority (100 por padrão) as ordena de outra forma. Com extensions, os plugins de um editor vêm depois dos plugins das extensions.
#As extensions do app
defineExtension cria uma. Suas funções rodam uma vez, quando um editor é criado com ela, recebem as opções da extension e retornam dados: commands, rules e plugins como as factories acima os criam.
import { StarterKit, command, createRichEditor, defineExtension, insertText, textRule } from "nsh/base";
const Signature = defineExtension({
name: "signature",
priority: 200,
addOptions: () => ({ text: "— Sent from NSH" }),
addCommands: (options) => ({ insertSignature: insertText(options.text) }),
addKeyboardShortcuts: () => ({ "Mod-Alt-s": command("insertSignature") }),
addInputRules: () => [textRule("\\(sig\\)$", "✍")],
});
const editor = createRichEditor({ extensions: [StarterKit, Signature.configure({ text: "Best, Ana" })] });
node ou mark (na forma de defineSchema) a torna uma extension de node ou de mark; addGlobalAttributes adiciona atributos a outros tipos; addProseMirrorPlugins retorna plugins, incluindo RichEditorPlugins; addPasteRules retorna rules textRule e markRule para texto colado. O extend({...}) de uma classe embutida substitui as partes que ela fornece: Underline.extend({ addKeyboardShortcuts: () => ({ "Mod-Alt-u": toggleMark("underline") }) }).
O crate de um app registra extensions escritas em Rust por meio de seu UI set, e o JS as obtém pelo nome:
use nsh_base::rich_editor::extensions::{Extension, RichExtensionRegistry};
#[native_module("app/native")]
fn native(registry: &mut Registry) {
registry.rich_extension("mention", || Extension::new("mention") /* .add_plugins(...) */);
}
const editor = createRichEditor({ extensions: [StarterKit, richExtension("mention", { char: "@" })] });
richExtension(name, options) também encontra toda extension embutida pelo nome ("text", "bold", …).
#Plugins
| Factory | O que adiciona |
|---|---|
history({ depth?, newGroupDelay? }) |
Histórico de undo; alterações dentro de newGroupDelay milissegundos (500) são desfeitas juntas |
keymap(bindings) |
Commands por nome de tecla: "Mod-b" (Cmd em plataformas Apple, Ctrl nas demais), "Shift-Enter", "Alt-ArrowUp" |
baseKeymap() |
Enter, Backspace, Delete, selecionar tudo e as teclas de edição de texto da plataforma |
starterKeymap() |
Os atalhos do schema inicial: marks, títulos, listas, blocos de código, hard breaks |
inputRules(rules) |
O texto digitado antes do cursor que corresponde a uma rule se transforma no que a rule produz; Backspace logo em seguida desfaz |
markdown(options?) |
Markdown enquanto se digita, colagem de Markdown e cópia de Markdown (blockRules, inlineRules, paste, copyAsMarkdown) |
starterKit() |
As input rules e os atalhos do starter, os atalhos base e o histórico |
A posição de um plugin na lista é sua prioridade: keymaps e input rules mais acima na lista tratam primeiro uma tecla ou um texto. As input rules vêm de markdownRules() (títulos, citações, listas, code fences, rules, as marks comuns), typographyRules() (aspas tipográficas, reticências, travessão), textRule(match, replacement), markRule(match, mark), wrappingRule(match, node) e textblockRule(match, node). O match de uma rule é uma expressão regular terminada em $, comparada com o texto antes do cursor.
#Os plugins do app
new RichEditorPlugin(key) cria um plugin cujo comportamento é do app, definido com chamadas encadeadas:
import { RichEditorPlugin, createRichEditor, starterKit } from "nsh/base";
const suggestions = new RichEditorPlugin("suggestions")
.captureKeys(["ArrowUp", "ArrowDown", "Enter", "Escape"], (key) => moveSuggestion(key))
.filterTransaction((tr) => tr.meta?.locked !== true)
.appendTransaction((trs, state) => null);
const editor = createRichEditor({ plugins: [starterKit(), suggestions] });
// While the suggestion list is open, its keys are the list's.
suggestions.capturing = true;
editor.setDecorations("suggestions", [{ from: 5, to: 9, kind: "inline", attrs: { class: "bg-accent" } }]);
captureKeys(keys, handler): enquantocapturingfor true, o editor consome essas teclas e informa ahandlerqual delas foi pressionada, logo em seguida; o teclado continua tão rápido quanto sem plugin.editor.setDecorations(key, decorations): atributos inline, atributos de node e widgets que a view desenha sobre o documento sem alterá-lo. Eles acompanham toda alteração até serem definidos novamente.filterTransaction(hook)eappendTransaction(hook)veem as transactions criadas pelas chamadas do próprio app (run,dispatch,setContent, …) naquela chamada:filterTransactionpode descartar uma, eappendTransactionpode retornar mais uma para ser aplicada junto.on("revision", …)ouve toda alteração, inclusive a digitação.
#Links, contagem de caracteres e sugestões
Três plugins trazem o comportamento de Link, CharacterCount e Suggestion do Tiptap, tudo nativo:
import { RichEditor, characterCount, createRichEditor, insertMention, link, starterKit, starterSchema, suggestion, withMention } from "nsh/base";
const editor = createRichEditor({
schema: withMention(starterSchema()),
plugins: [
starterKit(),
link({ defaultProtocol: "https" }),
characterCount({ limit: 280 }),
suggestion({ key: "mention", char: "@", decorationClass: "text-primary", captureKeys: ["ArrowUp", "ArrowDown", "Enter"] }),
],
});
editor.on("transaction", () => {
const s = editor.suggestion("mention");
if (s?.active) showPeople(s.query, editor.coordsAtPos(s.range.from), s.lastKey);
setCount(`${editor.characters()} / ${editor.characterLimit}`);
});
// The list picked someone:
editor.run(insertMention(editor.suggestion("mention")!.range, "u1", "Joan"));
link(options?): uma URL digitada antes de um espaço ou Enter vira link, assim como as URLs em texto colado (autolink); uma URL colada sobre uma seleção transforma a seleção em link (linkOnPaste); um clique ou toque em um link com Mod pressionado, ou enquanto o editor está em modo somente leitura, o abre por meio do tratamento de URLs do app (openOnClick);enableClickSelectionseleciona o link inteiro ao clicar. As URLs são encontradas com ou sem protocolo (nsh.dev,https://nsh.dev/docs, endereços de e-mail), e o protocolo que um host sem protocolo recebe édefaultProtocol(http). Os links aceitam os protocoloshttp,https,ftp,ftps,mailto,tel,callto,sms,cid,xmppe os definidos emprotocols, de modo quesetLink({ href }),toggleLink(attrs?)e a criação automática de links mantêm scripts (javascript:) e URLs de dados de fora.unsetLink()remove o link ao redor da seleção.characterCount(options?):editor.characters(mode?)conta os caracteres do texto ("textSize", unidades UTF-16 como olengthdo JS) ou as posições do documento ("nodeSize");editor.words()conta suas palavras (limites de palavra do Unicode). Comlimit, alterações que ultrapassariam o limite são recusadas e o conteúdo colado é cortado até o que couber; um primeiro documento acima do limite é cortado (autoTrim).setCharacterLimit(limit)altera o limite eeditor.characterLimito lê.suggestion(options?): acompanha um gatilho (char,@por padrão) digitado antes do cursor.allowSpacespermite que a consulta contenha espaços,allowedPrefixeslista o que pode preceder o gatilho (um espaço por padrão;anyPrefixaceita qualquer coisa),startOfLineexige o gatilho no início da linha (um menu de comandos/).editor.suggestion(key)fornece{ active, range, query, text, lastKey, keyCount }; o intervalo ativo carrega odecorationClassdo app (edecorationEmptyClassenquanto a consulta está vazia). Enquanto uma sugestão está ativa, as teclas emcaptureKeyspertencem à lista do app: o editor as captura e registra a última no estado, e Escape descarta a sugestão (exitSuggestion(key)também).replaceSuggestion(content, key?)substitui o gatilho e a consulta por texto ou conteúdo JSON.withMention(schema, { name?, char? })adiciona o nó de menção do Tiptap: um átomo inline comid,labele seu gatilho, escrito como@labelem texto e como<span data-type="mention">em HTML.insertMention(range, id, label?)substitui o intervalo de uma sugestão por uma menção e um espaço.
O popup é do app: ele lê o estado da sugestão, posiciona-se em coordsAtPos(range.from) e desenha os
itens à sua maneira.
#Tabelas
TableKit traz as tabelas do Tiptap: RichTable (Table do Tiptap), RichTableRow, RichTableHeader e
RichTableCell. Ele é adicionado junto a StarterKit:
import { RichEditor, StarterKit, TableKit, addRowAfter, createRichEditor, insertTable, mergeCells } from "nsh/base";
const editor = createRichEditor({
extensions: [StarterKit, TableKit.configure({ table: { cellMinWidth: 40 } })],
});
editor.run(insertTable({ rows: 3, cols: 3, withHeaderRow: true }));
editor.run(addRowAfter());
A tabela aceita as opções resizable (true), cellMinWidth (25), lastColumnResizable (true),
allowTableNodeSelection (false) e HTMLAttributes; TableKit.configure({ tableHeader: false }) deixa uma
extensão de fora. As células contêm blocos e têm colspan, rowspan e colwidth (uma largura em pontos para cada coluna
que abrangem); as linhas têm um height.
Tab e Shift-Tab movem entre as células, e Tab na última célula adiciona uma linha. As teclas de seta na borda de uma célula seguem para a
célula seguinte; Shift com uma seta, ou um arrasto sobre várias células, seleciona um retângulo de células (uma seleção do tipo
"cell", cujos anchor e head são as posições antes de sua primeira e de sua última célula). Backspace e Delete esvaziam
as células selecionadas e excluem a tabela quando todas as suas células estão selecionadas.
No editor, uma tabela é uma grade de suas células: cada coluna tem a largura do colwidth de suas células, e as colunas sem esse valor
dividem o espaço restante (com no mínimo cellMinWidth de largura), e cada linha tem no mínimo seu height. Sua aparência parte do tema: uma
grade de border de 1 ponto, células de cabeçalho em muted, células com preenchimento, e cada célula selecionada sob a parte selectedCell na
cor de seleção. classes.nodes (table, tableCell, tableHeader) e as partes reestilizam
tudo isso. Seta para cima e para baixo na primeira ou na última linha de uma célula levam à célula acima ou abaixo.
Enquanto o documento pode ser editado:
- Em desktops, a borda direita de cada coluna e a borda inferior de cada linha as redimensionam: o ponteiro mostra o
cursor de redimensionamento e a linha
tableResizeHandle, a tabela acompanha o arrasto, e ao soltar definecolwidthouheightem uma única etapa que o desfazer reverte.resizable: falsedesativa isso,lastColumnResizable: falsemantém a largura da última coluna. - A tabela em que a seleção está mostra alças (
tableGrip) em sua borda superior sobre cada coluna e em sua borda esquerda ao lado de cada linha. Pressionar uma delas seleciona aquela coluna ou linha; arrastá-la em desktops mostra onde ela será solta (tableDropIndicator) e a move para lá ao soltar.
<RichEditor
editor={editor}
classes={{
nodes: { tableHeader: "font-semibold", tableCell: "px-3 py-2" },
selectedCell: "bg-primary/10",
tableGrip: "bg-muted-foreground/60 rounded-full",
}}
/>
Os comandos, cada um um auxiliar de nsh/base:
insertTable({ rows, cols, withHeaderRow })(3 × 3 com uma linha de cabeçalho por padrão) edeleteTable().addColumnBefore(),addColumnAfter(),deleteColumn(),addRowBefore(),addRowAfter(),deleteRow().mergeCells(),splitCell()emergeOrSplit().toggleHeaderRow(),toggleHeaderColumn()etoggleHeaderCell().setCellAttribute(name, value),setCellSelection({ anchorCell, headCell }),goToNextCell(),goToPreviousCell()efixTables(), que repara tabelas com células ausentes ou sobrepostas.moveTableRow({ from, to, select })emoveTableColumn({ from, to, select })movem uma linha ou coluna, junto com as linhas ou colunas que suas células mescladas abrangem;setColumnWidth({ col, width })esetRowHeight({ row, height })as dimensionam (nullas dimensiona de acordo com o conteúdo). Eles aceitamtable, a posição antes de uma tabela, para agir em uma tabela diferente da da seleção.
As tabelas são lidas e gravadas em HTML (<table> com <th> / <td>, colspan, rowspan e data-colwidth) e em
Markdown: uma tabela com linha de cabeçalho cujas células têm cada uma um parágrafo é uma tabela GFM com barras verticais, e qualquer outra tabela é
gravada como seu HTML, que é lido de volta da mesma forma. Em texto simples, uma linha é uma linha de texto com suas células separadas por tabulações.