API dokumentace

Nibble je rozdělený do tří balíčků: @nibble/core (model, výběr, historie, příkazy, registr ovládání), @nibble/ui (lišta, nabídka, dialogy, CSS) a @nibble/plugins (odkazy, obrázky, tabulky, videa, nástroje, písma, úkoly, Markdown při psaní). Jádro běží i bez lišty — hodí se to v testech a při dávkovém zpracování obsahu.

Instalace

git clone https://github.com/svatekr70/nibble
npm install
npm run build

Build vytvoří ESM bundly a stylopis. Do stránky se vkládají takhle:

import { Nibble } from '@nibble/core'
import { attachToolbar } from '@nibble/ui'
import '@nibble/ui/nibble.css'
Styly se importují zvlášť

Knihovna do stránky sama nic nevkládá. Buď načtěte nibble.css, nebo si napište vlastní — všechno používá jen třídy s prefixem nb- a proměnné --nb-*.

Bez balíčkovače (CDN)

Sestavená knihovna je jeden soubor se vším, co je v core, ui i plugins. Nic se neinstaluje a nic se nesestavuje — stačí adresa:

<link rel="stylesheet"
      href="https://cdn.jsdelivr.net/gh/svatekr70/nibble@v0.6.0/dist/nibble.css">

<div id="obsah"><p>Ahoj.</p></div>

<script type="module">
  import { Nibble, attachToolbar, link, image, table }
    from 'https://cdn.jsdelivr.net/gh/svatekr70/nibble@v0.6.0/dist/nibble.min.js'

  const editor = await Nibble.create({
    target: '#obsah',
    plugins: [link, image, table],
  })
  attachToolbar(editor, { menubar: true })
</script>

Je to modul, takže <script> musí mít type="module".

Verzi v adrese si pište vždycky

@v0.6.0 je neměnné: ten soubor už se nikdy nezmění a jsDelivr ho drží v mezipaměti natrvalo. Bez verze (@main) se tahá poslední stav hlavní větve — dobré na zkoušení, nebezpečné v ostrém provozu.

Proto je dist/ v repozitáři, i když se sestavuje: jsDelivr servíruje soubory přímo z tagu a bez commitnutého buildu by ta adresa neexistovala. Že bundle sedí se zdrojem, hlídá CI — po sestavení musí být pracovní strom čistý.

Totéž leží i tady na webu (https://svatekr70.github.io/nibble/dist/nibble.min.js), jen je to vždy poslední stav hlavní větve. Jednotlivé balíčky jdou z webu načíst i zvlášť — dist/core/src/index.js, dist/ui/src/index.js, dist/plugins/src/index.js — když nechcete tahat všechno.

Rychlý start

import { Nibble } from '@nibble/core'
import { attachToolbar } from '@nibble/ui'
import { link, createImagePlugin, table } from '@nibble/plugins'
import '@nibble/ui/nibble.css'

const editor = await Nibble.create({
  target: '#obsah',
  schema: 'legacy',
  plugins: [
    link,
    createImagePlugin({
      // Bez adaptéru se obrázek vloží jako data: URL.
      upload: async (file) => {
        const body = new FormData()
        body.append('file', file)
        const res = await fetch('/api/upload', { method: 'POST', body })
        return (await res.json()).url
      },
    }),
    table,
  ],
})

attachToolbar(editor)

editor.on('change', ({ html }) => uloz(html))

Při napojení na <textarea> zůstává textarea zdrojem pravdy pro odeslání formuláře — stačí vyměnit jeden řádek a odesílání funguje dál.

Nibble.create()

Nibble.create(config: NibbleConfig): Promise<Editor>

Vrací Promise, protože pluginy smí mít asynchronní přípravu (třeba načtení seznamu písem). Existuje i pojmenovaný export create() se stejným chováním.

target je CSS selektor, prvek nebo <textarea>. U textarey se převezme její obsah, textarea se schová a při každé změně se do ní zapisuje výsledek — odeslání formuláře pak funguje beze změny v backendu.

Přehled voleb

VolbaTypVýznam
targetstring | HTMLElementKam se editor postaví. Povinné.
contentstringPočáteční HTML. Bez něj se převezme obsah cílového prvku.
schema'strict' | 'legacy'Jak přísně se hlídá tvar obsahu. Výchozí legacy.
entityEncoding'named' | 'utf8' | 'auto'Jak se zapisují entity. auto se řídí tím, co bylo v původním obsahu.
heightnumberMinimální výška plochy s obsahem v pixelech.
readonlybooleanEditor se otevře jen ke čtení. Jde přepnout přes setMode().
autofocusbooleanZaostřit obsah hned po vytvoření.
placeholderstringŠedá nápověda v prázdném editoru. Bez uvedení se převezme z <textarea placeholder>. Je jen v CSS, do getHTML() se nedostane.
pluginsPlugin[]Hodnoty, ne jména. Bundler tak vidí, co se opravdu použije.
pastePasteOptionsChování při vkládání ze schránky — viz níž.
allowedEmbedHostsstring[]Hostitelé, jejichž <iframe> smí být v obsahu.

Schema

Schema říká, co smí kde být. Dva režimy:

RežimChování
legacyPouští dál i značky a atributy, které by dnes nikdo nenapsal — <font>, align, <center>. Pro obsah, který vznikal roky v jiných nástrojích.
strictSémantické značky a inline styly. Pro nový obsah a nové projekty.

Srovnávání tvaru je líné: běží jen na tom bloku, do kterého uživatel opravdu zasáhl. Načtený dokument se nikdy nepřepisuje jen proto, že by se dnes zapsal jinak.

editor.audit(html?: string): SchemaViolation[]

Zkontroluje obsah proti schématu, aniž by cokoli měnil. Bez argumentu kontroluje aktuální obsah editoru. Hodí se na dávkovou revizi databáze před migrací.

Vkládání

await Nibble.create({
  target: '#obsah',
  paste: {
    keepStyles: ['color', 'background-color', 'text-align'],
    markdown: true,
  },
})
VolbaTypVýznam
keepStylesstring[]Které vlastnosti stylu si z vloženého obsahu nechat.
allowedTagsSet<string>Povolené značky. Co v seznamu není, se rozbalí a text zůstane.
markdownbooleanPřevádět Markdown, když přijde čistý text. Výchozí true.

Zdroj vloženého obsahu se rozpoznává a čistí se podle něj. Událost pasteclean nese, co se stalo:

editor.on('pasteclean', ({ source, removed }) => {
  // source: 'word' | 'gdocs' | 'libreoffice' | 'html' | 'text'
  console.log(source, removed)
})

Vložené rámce

<iframe> je jediná značka, která do stránky pouští cizí kód. Nibble ho proto neřeší seznamem zakázaného, ale seznamem povoleného — co v něm není, se zahodí:

await Nibble.create({
  target: '#obsah',
  // Prázdné pole zakáže rámce úplně.
  allowedEmbedHosts: ['youtube.com', 'youtube-nocookie.com', 'vimeo.com'],
})

Bez uvedení platí DEFAULT_EMBED_HOSTS — běžné videoslužby. Kontrola porovnává hostitele včetně poddomén, ne podřetězec v URL.

Obsah

editor.getHTML(): string

Serializovaný obsah. Bloky, kterých se nikdo nedotkl, se vypíšou v původním znění — proto je výsledek po pouhém načtení znak po znaku shodný se vstupem.

editor.setHTML(html: string, entityEncoding?: EntityEncoding): void

Nahradí obsah. Zahodí historii oblastí a založí nové snímky, takže záruka round-tripu platí i pro nově nastavený obsah.

editor.getText(): string
editor.insertHTML(html: string): boolean

Vloží HTML na pozici kurzoru. Obsah projde sanitizací, takže je bezpečné sem poslat i něco, co přišlo zvenčí.

editor.isDirty(): boolean
editor.getDirtyBlocks(): Node[]

Které uzly nejvyšší úrovně se od načtení změnily. Užitečné při ladění i v testech — demo na tomhle webu jimi ukazuje, kolik bloků úprava opravdu zasáhla.

Stav a služby

VlastnostK čemu je
editor.rootPrvek s contenteditable. Sem míří všechno, co se dotýká DOMu.
editor.documentDokument, ve kterém editor žije. Příkazy z něj vytvářejí uzly.
editor.selectiongetRange(), setRange(), collapseTo(), save(), restore().
editor.schemaKontrola tvaru obsahu.
editor.historyundo(), redo(), canUndo(), transact(fn) — zápisy do historie uvnitř se zahodí, takže úprava složená z více příkazů je po editor.commit() jeden krok. Jde vnořit.
editor.formatterapply(), remove(), toggle(), match(), queryStyle().
editor.commandsRegistr příkazů: add(), has().
editor.uiRegistr ovládání, dialogy, hlášky, stavový řádek.
editor.pluginsJména načtených pluginů v pořadí, ve kterém se nastavovaly.

Akce a dotazy

editor.exec('link', { href })   // → boolean (šlo to?)
editor.can('link')              // smí se to teď? → stav tlačítka
editor.is('bold')               // je výběr tučný? → aktivní tlačítko
editor.focus()
editor.setMode('design')        // 'design' | 'readonly'
editor.destroy()

Proti TinyMCE zmizel globální singleton: žádné nibble.activeEditor. Instanci si drží ten, kdo ji vytvořil.

Pomocné dotazy na aktuální blok používá lišta a hodí se i vlastnímu ovládání:

editor.getBlockTag()      // 'p' | 'h2' | 'blockquote' | null
editor.isInList()         // 'ul' | 'ol' | null
editor.getAlignment()     // 'left' | 'center' | 'right' | 'justify' | ''
editor.currentBlock()     // Element | null

bullist a numlist berou nepovinné { type } — data-type nově zakládaného seznamu. Nastaví se dřív, než se seznam slije se sousedy; sousední seznamy se totiž slévají jen při stejném data-type, takže úkoly pod odrážkami zůstanou úkoly.

editor.exec('bullist', { type: 'taskList' })
editor.exec('tasklist')      // tasks: přepne seznam úkolů
editor.exec('details')       // details: obalí označené bloky
editor.exec('detailsopen')   // přepne atribut open
editor.exec('undetails')     // zruší blok, obsah zůstane

Blokové příkazy (nadpis, seznam, citace) v nadpisu rozbalovacího bloku (<summary>) nic neudělají a vrátí false — nadpis je jeden řádek.

Pro pluginy, které reagují na psaní:

textBeforeCaret(block: Element, range: Range): string

Text bloku od začátku po kurzor, i když je blok rozdělený do více uzlů a značek. Na něm stojí Markdown zkratky i lomítková nabídka.

isEmptyDocument(root: Element): boolean

Je v editoru jen jeden prázdný odstavec? Podle toho se ukazuje placeholder.

Události

on() vrací funkci pro odhlášení — nemusíte si držet referenci na posluchače:

const off = editor.on('change', ({ html }) => uloz(html))
off()
UdálostKdy nastane
changeObsah se změnil a změna se uzavřela do kroku historie. Nese { html }.
inputKaždý zásah do obsahu, ještě před uzavřením kroku.
selectionchangeKurzor nebo výběr se pohnul. Podle toho se sesynchronizuje lišta.
setcontentObsah byl nahrazen přes setHTML().
modechangePřepnul se režim (design / readonly).
pastecleanDoběhlo čištění vloženého obsahu. Nese { source, removed, html }.
schemaviolationDo obsahu se dostalo něco, co schema nepovoluje.
limitPočet znaků přešel přes maxChars nebo se vrátil pod něj (createWordcountPlugin). Nese { count, max, over }. Jen při přechodu, ne po každém úhozu.
itemsplitEnter rozdělil položku seznamu. Nese { from, to } — původní a novou <li>.
listretagbullist / numlist přepnul značku existujícího seznamu. Nese { list, tag }.

attachToolbar()

attachToolbar(editor: Editor, options?: AttachOptions): EditorUI
const ui = attachToolbar(editor, {
  layout: [
    ['undo', 'redo'],
    ['bold', 'italic', 'underline'],
    ['link', 'image', 'table'],
  ],
  layoutBottom: [['code', 'fullscreen']],
  menubar: true,
  prefsKey: 'clanky',
  prefs: { height: '500px' },
  settings: true,
})
VolbaTypVýznam
layoutstring[][]Skupiny tlačítek v prvním řádku lišty.
layoutBottomstring[][]Skupiny, které mají začít ve druhém řádku.
menubarboolean | MenubarMenu[]Nabídkový pruh nad lištou. true použije výchozí rozvržení.
prefsKeystringKlíč pro uložení nastavení uživatele (nibble:prefs:<id>).
prefsPartial<EditorPrefs>Výchozí hodnoty nastavení. Uživatel je může přebít.
settingsbooleanOzubené kolo vpravo nahoře. Jediné místo, kde se dá schovat.

Vrácené EditorUI nese toolbar, menubar, contextToolbar, prefs a destroy().

Výchozí rozvržení obsahuje: undo redo · blocks fontfamily fontsize lineheight · bold italic underline strike · forecolor backcolor · bullist numlist tasklist · alignleft aligncenter alignright alignjustify · link image media table emoji charmap · blockquote hr removeformat · code searchreplace fullscreen. Tlačítko, jehož plugin není načtený, se v liště prostě neobjeví.

Registr ovládání

Plugin si přihlásí ovládací prvek a lišta ho vykreslí. Žádné další zařizování není potřeba — a jádro nemusí o liště vědět.

editor.ui.addButton('zvyraznit', {
  icon: 'forecolor',
  tooltip: 'Zvýraznit',
  shortcut: 'Ctrl+H',
  onAction: () => editor.exec('zvyraznit'),
  active: (ed) => ed.is('zvyraznit'),
  enabled: (ed) => ed.can('zvyraznit'),
})
MetodaVykreslí se jako
addButton(name, spec)Tlačítko, případně přepínač.
addSelect(name, spec)Rozbalovací seznam (druh bloku, písmo, velikost).
addColor(name, spec)Výběr barvy s kolečkem a paletami.
addMenu(name, spec)Tlačítko s nabídkou položek.
addGrid(name, spec)Mřížka — používá ji výběr rozměru tabulky.
addContextToolbar(name, spec)Plovoucí lišta nad vybraným prvkem.
addSuggestion(name, spec)Nabídka u kurzoru vyvolaná znakem (lomítkové příkazy).

Nabídka u kurzoru

addSuggestion je obecná verze lomítkové nabídky — jiný znak, jiné položky. Fokus zůstává v editoru: psaní za spouštěčem nabídku filtruje, šipky vybírají, Enter nebo Tab potvrdí, Escape zavře.

import { filterSuggestions } from '@nibble/core'

editor.ui.addSuggestion('mention', {
  trigger: '@',
  items: (ed, query) => filterSuggestions(lide, query),
  onPick: (ed, item) => ed.exec('mention', item.id),
})
Položka SuggestionSpecVýznam
triggerZnak, který nabídku otevře.
startOfBlockJen na začátku bloku, jako lomítko. Jinak stačí mezera před spouštěčem.
items(editor, query)Položky pro napsaný dotaz. Prázdné pole nabídku schová.
onPick(editor, item)Vybraná položka. Napsaný dotaz už je smazaný.
opensDialog(item)Otevírá položka dialog? Smazání dotazu se pak zapíše jako samostatný krok, takže zavřený dialog nechá prázdný řádek.

Položka (SuggestionItem) má id, label a nepovinně icon a keywords. filterSuggestions(items, query) hledá bez ohledu na diakritiku a velikost písmen a dává napřed položky, jejichž popisek dotazem začíná. suggestionQuery(textBefore, trigger, startOfBlock) vrátí dotaz za spouštěčem, nebo null, když nabídka otevřená být nemá.

Data jsou v jádře, vykresluje je bindCaretMenu(editor) z @nibble/ui. attachToolbar() ho zapojí sám; ručně je potřeba jen bez lišty. Vrací funkci pro úklid.

editor.ui.get(name) vrátí přihlášený prvek, names() seznam všech. Toho využívá i nastavení, aby nenabízelo tlačítka, jejichž plugin se nenačetl.

Dialogy a hlášky

editor.ui.dialog(spec: DialogSpec): Promise<Record<string, unknown> | null>

Dialog se popíše polem polí; vykreslení, ozvučení i validaci obstará lišta. Vrací null, když uživatel dialog zavřel.

const data = await editor.ui.dialog({
  title: 'Vložit odkaz',
  fields: [
    { type: 'url', name: 'href', label: 'Adresa', required: true },
    { type: 'text', name: 'text', label: 'Text odkazu' },
    { type: 'checkbox', name: 'blank', label: 'Otevřít v novém okně' },
  ],
  submit: 'Vložit',
})

if (data) editor.exec('link', data)

Typy polí: text, url, number, select, checkbox, color, textarea, code (velké okno se zvýrazněnou syntaxí) a dvojice emoji / chars (mřížka pojmenovaných znaků s kategoriemi a hledáním; seznam se předává v field.glyphs, liší se jen sazbou políček).

Výběr přežije otevření dialogu

Otevření modálního okna přesune fokus a s ním i výběr. ui.dialog() si ho proto uloží a před spuštěním příkazu vrátí zpátky — jednou pro všechny dialogy, aby se na to nedalo zapomenout v jednotlivých pluginech.

editor.ui.notify(text: string, level?: 'info' | 'warn' | 'error'): void
editor.ui.setStatus(name: string, text: string | null, tone?: 'normal' | 'over'): void

Stav se ukládá i tehdy, když stavový řádek zrovna neexistuje. Když si ho uživatel zapne, hodnoty se do něj přehrají — plugin se nemusí spouštět znovu. 'over' položku zbarví jako překročenou; tak počítadlo ukazuje limit.

Nastavení uživatele

Rozvržení lišty navrhne programátor, ale poslední slovo má ten, kdo u editoru sedí. Nastavení se ukládá do localStorage a přebíjí konfiguraci.

ui.prefs.get()                     // aktuální nastavení
ui.prefs.set({ sticky: false })      // ovládání se postaví znovu
ui.prefs.layoutFor('top')            // co se vykreslí v prvním řádku
ui.prefs.reset()                   // zpátky podle konfigurace
ui.prefs.onChange(fn)               // vrací funkci pro odhlášení
PoložkaTypVýchozíVýznam
widthstring''Šířka editoru. Prázdné = podle obsahu.
heightstring''Výška plochy s obsahem. Zadaná výška zapne rolování uvnitř.
menubarbooleanfalseNabídkový pruh.
stickybooleantrueLišta se drží u horního okraje.
statusbarbooleantrueInformační řádek s cestou k prvku.
resizablebooleantrueZměna velikosti tažením za pravý dolní roh.
groupsPrefGroup[]z layoutSkupiny tlačítek, jejich pořadí, zapnutí a řádek.
Uloženému nastavení se nevěří slepě

Sedí v prohlížeči uživatele a může být staré půl roku. Slučuje se proto s aktuální konfigurací: prvek, který uživatel zná, si drží pořadí i zapnutí, prvek, který mezitím přibyl, se doplní na konec své skupiny. Bez toho by upgrade editoru znamenal, že nové tlačítko nikdo nikdy neuvidí.

Dialog nastavení umí vypsat hotovou konfiguraci podle aktuálního stavu — když si uživatel lištu přeskládá tak, že to dává smysl, je to nejlepší podklad pro to, jak má editor vypadat pro všechny ostatní.

Dodávané pluginy

PluginPřináší
linkVložení a úprava odkazu, plovoucí lišta nad odkazem, Ctrl+K.
image / createImagePlugin({ upload })Obrázky. Bez adaptéru jako data: URL, s adaptérem nahrání na server.
tableTabulky: mřížkový model, řádky a sloupce, slučování, tažení šířky sloupce, vlastnosti tabulky i řádku.
mediaVidea přes <iframe> z povolených hostitelů.
emoji / createEmojiPlugin({ emoji })Emotikony: mřížka s kategoriemi a hledáním, česky. Vkládá znak, ne obrázek.
charmap / createCharmapPlugin({ chars })Mapa speciálních znaků v téže mřížce: interpunkce, mezery, měny, matematika, zlomky, šipky, diakritika, řecká abeceda, symboly. Hledá i podle U+00A9.
codeEditace zdrojového HTML se zvýrazněnou syntaxí a pamatováním pozice kurzoru.
autolinkZ napsané adresy udělá odkaz po mezeře nebo Enteru. Adresa nebo e-mail vložený ze schránky je rovnou odkaz; vložený na označený text z něj udělá odkaz bez dialogu. V <pre> a <code> zůstane textem.
wordcount / createWordcountPlugin({ maxChars })Počet slov a znaků do stavového řádku. S maxChars ukáže „152 / 160 znaků“ (včetně mezer, jako maxlength), po překročení zčervená a vyšle limit. Nic neusekne. Stejný výpočet je veřejný jako wordcountLabel(text, maxChars?) → { label, count, over }.
fullscreenEditor přes celou obrazovku, F11.
searchreplaceHledání a nahrazování v textu, ne ve značkách. Ctrl+F.
typographyČeské uvozovky, výpustka, pomlčka — při psaní.
fontsRodina a velikost písma včetně webových písem z Google Fonts.
markdownShortcutsMarkdown při psaní: ## udělá nadpis, **slovo** tučné slovo. První Ctrl+Z převod vrátí. Seznam zkratek je níž.
tasksTlačítko a příkaz tasklist: seznam úkolů <ul data-type="taskList"> s <li data-type="taskItem" data-checked>, formát jako TipTap. Zaškrtávátko kreslí jen CSS editoru.
detailsRozbalovací blok <details><summary>: příkazy details, detailsopen, undetails a plovoucí lišta. V editoru je obsah vždy rozbalený.
slash/ na začátku řádku otevře nabídku: nadpisy, seznamy, úkoly, citace, kód, čára, tabulka, obrázek, video, rozbalovací blok. Ukazuje jen to, co je v editoru zapnuté.
dragHandleÚchyt ⠿ vlevo u bloku: tažením blok přesune, klikem ho označí. Přesun je jeden krok historie a blok se nemění. Na dotyku se neukazuje.

Vlastní plugin

Plugin je objekt se jménem a funkcí setup. Ta dostane celý editor — jakýkoli mezistupeň by musel jen předávat dál. Vrácená funkce je úklid; volá se v destroy(), takže inicializace i úklid stojí na jednom místě.

import type { Plugin } from '@nibble/core'

export const poznamka: Plugin = {
  name: 'poznamka',

  setup(editor) {
    editor.commands.add('poznamka', (ed) =>
      ed.insertHTML('<aside class="pozn">…</aside>'))

    editor.ui.addButton('poznamka', {
      icon: 'blockquote',
      tooltip: 'Poznámka na okraj',
      onAction: () => editor.exec('poznamka'),
    })

    const off = editor.on('change', spocitejPoznamky)
    return () => { off() }
  },
}

Pluginy s volbami se dělají tovární funkcí — createImagePlugin(), createTablePlugin(), createMediaPlugin(), createFontPlugin() — aby se konfigurace nemusela protahovat globálním stavem.

Serializace a round-trip

Parser si u každé oblasti nejvyšší úrovně pamatuje původní zdroj a snímek DOMu. Serializace pak vypíše zdroj doslova — dokud se DOM od snímku neliší. Znamená to mimo jiné, že:

  • entity zůstanou tak, jak byly (&nbsp; se nepřepíše na mezeru a naopak),
  • pořadí a zápis atributů se nemění, včetně uvozovek,
  • komentáře, podmíněné komentáře i podivné mezery přežijí,
  • úprava jednoho odstavce nepřeformátuje zbytek dokumentu.

Jediná změna, která se dělá vždy, je převod CRLF na LF. Prohlížeč ho udělá sám v textarea.value i při parsování HTML — slib, že se zachová, by se nedal splnit. Pomocná funkce normalizeNewlines() je veřejná, aby šlo porovnávat se stejným základem.

serializeNode(node: Node, options?: SerializeOptions): string
sanitize(html: string, options?: SanitizeOptions): SanitizeResult

Obojí je dostupné samostatně — hodí se při dávkovém zpracování obsahu mimo editor.

CSS proměnné

Žádný generátor motivů, jen proměnné. Sedí na :root, protože dialogy se vkládají do <body> — mimo obal editoru — a jinak by je nezdědily.

ProměnnáSvětláK čemu je
--nb-bg#ffffffPozadí lišty, dialogů a plochy.
--nb-fg#16191cZákladní barva textu.
--nb-muted#6b7280Popisky, nápovědy, stavový řádek.
--nb-border#dcdfe3Rámečky a oddělovače.
--nb-hover#f1f3f5Podbarvení pod myší.
--nb-active#e3ecfbZapnuté tlačítko.
--nb-accent#1f5f5bPřízvučná barva, fokus, hlavní tlačítko.
--nb-radius6pxZaoblení rohů.
--nb-selectionprůsvitnáOznačený text ve zdrojovém kódu. Musí zůstat průsvitná, jinak zmizí obarvená syntaxe.

Tmavý režim se řídí prefers-color-scheme a přepisuje stejné proměnné. Kdo chce vlastní vzhled, přebije je ve svém CSS.

Klávesové zkratky

ZkratkaAkce
Ctrl+B / I / UTučně, kurzíva, podtržení.
Ctrl+ZZpět.
Ctrl+Shift+ZZnovu.
Ctrl+KVložit nebo upravit odkaz.
Ctrl+FHledat a nahradit.
F11Celá obrazovka.
Tab / Shift+TabV seznamu zanoří a vysune položku, v tabulce přejde na další buňku.
Enter / Shift+EnterNový blok / zalomení řádku. V nadpisu rozbalovacího bloku skočí do obsahu.
Ctrl+Shift+VVložit jako čistý text — i adresu, ze které by jinak byl odkaz.
/ na začátku řádkuLomítková nabídka (slash). Psaní filtruje, šipky vybírají, Enter / Tab potvrdí, Esc zavře a text nechá.
klik do zaškrtávátkaPřepne úkol (tasks). Enter na zaškrtnutém úkolu založí nový nezaškrtnutý.
Esc při tažení blokuZruší přesun (dragHandle).

Markdown při psaní

S pluginem markdownShortcuts. Blokové zkratky platí, jen když je vzor celý začátek odstavce:

NapíšuSpouštíVýsledek
#–######mezeranadpis h1–h6
-, *mezeraodrážkový seznam
1. (libovolné číslo)mezeračíslovaný seznam; číslo ≠ 1 nastaví start
>mezeracitace
[ ], [x]mezeraúkol nezaškrtnutý / zaškrtnutý (jen s tasks)
---Enter<hr> a pod ním nový odstavec
```Enter<pre>

Řádkové se spouštějí zavírací značkou: **x** tučně, *x* a _x_ kurzíva, `x` kód, ~~x~~ přeškrtnutí. Obsah nesmí začínat ani končit mezerou (2 * 3 * 4 je násobení) a _ uprostřed slova nic nespustí. V kódu a v odkazech se zkratky nespouštějí. První Ctrl+Z převod vrátí a nechá napsaný text.

Zkratky pluginů se objevují i v nabídkovém pruhu vedle názvu položky, takže se dají odkoukat bez čtení dokumentace.