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). 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'
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.5.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.5.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".
@v0.5.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()
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
| Volba | Typ | Význam |
|---|---|---|
target | string | HTMLElement | Kam se editor postaví. Povinné. |
content | string | Počá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. |
height | number | Minimální výška plochy s obsahem v pixelech. |
readonly | boolean | Editor se otevře jen ke čtení. Jde přepnout přes setMode(). |
autofocus | boolean | Zaostřit obsah hned po vytvoření. |
plugins | Plugin[] | Hodnoty, ne jména. Bundler tak vidí, co se opravdu použije. |
paste | PasteOptions | Chování při vkládání ze schránky — viz níž. |
allowedEmbedHosts | string[] | Hostitelé, jejichž <iframe> smí být v obsahu. |
Schema
Schema říká, co smí kde být. Dva režimy:
| Režim | Chování |
|---|---|
legacy | Pouš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. |
strict | Sé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.
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, }, })
| Volba | Typ | Význam |
|---|---|---|
keepStyles | string[] | Které vlastnosti stylu si z vloženého obsahu nechat. |
allowedTags | Set<string> | Povolené značky. Co v seznamu není, se rozbalí a text zůstane. |
markdown | boolean | Př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
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.
Nahradí obsah. Zahodí historii oblastí a založí nové snímky, takže záruka round-tripu platí i pro nově nastavený obsah.
Vloží HTML na pozici kurzoru. Obsah projde sanitizací, takže je bezpečné sem poslat i něco, co přišlo zvenčí.
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
| Vlastnost | K čemu je |
|---|---|
editor.root | Prvek s contenteditable. Sem míří všechno, co se dotýká DOMu. |
editor.document | Dokument, ve kterém editor žije. Příkazy z něj vytvářejí uzly. |
editor.selection | getRange(), setRange(), collapseTo(), save(), restore(). |
editor.schema | Kontrola tvaru obsahu. |
editor.history | undo(), redo(), canUndo(). |
editor.formatter | apply(), remove(), toggle(), match(), queryStyle(). |
editor.commands | Registr příkazů: add(), has(). |
editor.ui | Registr ovládání, dialogy, hlášky, stavový řádek. |
editor.plugins | Jmé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
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álost | Kdy nastane |
|---|---|
change | Obsah se změnil a změna se uzavřela do kroku historie. Nese { html }. |
input | Každý zásah do obsahu, ještě před uzavřením kroku. |
selectionchange | Kurzor nebo výběr se pohnul. Podle toho se sesynchronizuje lišta. |
setcontent | Obsah byl nahrazen přes setHTML(). |
modechange | Přepnul se režim (design / readonly). |
pasteclean | Doběhlo čištění vloženého obsahu. Nese { source, removed, html }. |
schemaviolation | Do obsahu se dostalo něco, co schema nepovoluje. |
attachToolbar()
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, })
| Volba | Typ | Význam |
|---|---|---|
layout | string[][] | Skupiny tlačítek v prvním řádku lišty. |
layoutBottom | string[][] | Skupiny, které mají začít ve druhém řádku. |
menubar | boolean | MenubarMenu[] | Nabídkový pruh nad lištou. true použije výchozí rozvržení. |
prefsKey | string | Klíč pro uložení nastavení uživatele (nibble:prefs:<id>). |
prefs | Partial<EditorPrefs> | Výchozí hodnoty nastavení. Uživatel je může přebít. |
settings | boolean | Ozubené 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 ·
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'), isActive: () => editor.is('zvyraznit'), isEnabled: () => editor.can('zvyraznit'), })
| Metoda | Vykreslí 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. |
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
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).
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.
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.
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žka | Typ | Výchozí | Význam |
|---|---|---|---|
width | string | '' | Šířka editoru. Prázdné = podle obsahu. |
height | string | '' | Výška plochy s obsahem. Zadaná výška zapne rolování uvnitř. |
menubar | boolean | false | Nabídkový pruh. |
sticky | boolean | true | Lišta se drží u horního okraje. |
statusbar | boolean | true | Informační řádek s cestou k prvku. |
resizable | boolean | true | Změna velikosti tažením za pravý dolní roh. |
groups | PrefGroup[] | z layout | Skupiny tlačítek, jejich pořadí, zapnutí a řádek. |
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
| Plugin | Přináší |
|---|---|
link | Vlož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. |
table | Tabulky: mřížkový model, řádky a sloupce, slučování, tažení šířky sloupce, vlastnosti tabulky i řádku. |
media | Videa 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. |
code | Editace zdrojového HTML se zvýrazněnou syntaxí a pamatováním pozice kurzoru. |
autolink | Z napsané adresy udělá odkaz po mezeře nebo Enteru. |
wordcount | Počet slov a znaků do stavového řádku. |
fullscreen | Editor přes celou obrazovku, F11. |
searchreplace | Hledání a nahrazování v textu, ne ve značkách. Ctrl+F. |
typography | České uvozovky, výpustka, pomlčka — při psaní. |
fonts | Rodina a velikost písma včetně webových písem z Google Fonts. |
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', { exec() { return editor.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 (
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.
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 | #ffffff | Pozadí lišty, dialogů a plochy. |
--nb-fg | #16191c | Základní barva textu. |
--nb-muted | #6b7280 | Popisky, nápovědy, stavový řádek. |
--nb-border | #dcdfe3 | Rámečky a oddělovače. |
--nb-hover | #f1f3f5 | Podbarvení pod myší. |
--nb-active | #e3ecfb | Zapnuté tlačítko. |
--nb-accent | #1f5f5b | Přízvučná barva, fokus, hlavní tlačítko. |
--nb-radius | 6px | Zaoblení rohů. |
--nb-selection | prů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
| Zkratka | Akce |
|---|---|
| Ctrl+B / I / U | Tučně, kurzíva, podtržení. |
| Ctrl+Z | Zpět. |
| Ctrl+Shift+Z | Znovu. |
| Ctrl+K | Vložit nebo upravit odkaz. |
| Ctrl+F | Hledat a nahradit. |
| F11 | Celá obrazovka. |
| Tab / Shift+Tab | V seznamu zanoří a vysune položku, v tabulce přejde na další buňku. |
| Enter / Shift+Enter | Nový blok / zalomení řádku. |
Zkratky pluginů se objevují i v nabídkovém pruhu vedle názvu položky, takže se dají odkoukat bez čtení dokumentace.