Überall integrierbar
Sumaq ist ein einziges ES-Modul, welches ihr importiert und auf einen Container zeigen lasst und das war's. Sumaq funktioniert in jedem Browser und in jedem Kontext: PHP-Backends, React-Komponente, Vue, Svelte, native Web Komponente oder eben als vanilla JS Modul. Keine Abhängigkeiten, kein Vendor-Lock-in und faire Preise.
Vanilla JS
Das Setup ohne viel Aufwand. Ein Container mit einer ID, ein Import und ihr könnt losschreiben.
<div id="editor"></div>
<script type="module">
import Sumaq from '/path/to/sumaq.js';
const editor = new Sumaq('#editor', {
toolbar: ['h2', 'h3', 'p', '|', 'bold', 'italic', '|', 'link']
});
</script>
Die Instanz gibt euch Zugriff auf Inhalt und Events:
// Inhalt lesen
editor.getHTML();
editor.getText();
// Inhalt setzen
editor.setHTML('<p>Neuer Inhalt</p>');
// Auf Events reagieren
editor.on('input', (html) => {
console.log('Inhalt geändert:', html);
});
// Theme wechseln
editor.setTheme('elegant');
Alle Methoden und Events findet ihr in der API-Referenz.
Alles in einer Datei
Ihr könnt das alles in einer einzigen HTML-Datei zusammensetzen. Der Editor oben, die Vorschau unten und beim Schreiben wird die Ausgabe live aktualisiert:
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Sumaq Editor</title>
</head>
<body>
<div id="editor"></div>
<h2>Vorschau</h2>
<output id="preview"></output>
<script type="module">
import Sumaq from './sumaq.js';
const editor = new Sumaq('#editor', {
toolbar: ['h2', 'h3', 'p', '|', 'bold', 'italic', '|', 'link']
});
editor.on('input', (html) => {
document.getElementById('preview').innerHTML = html;
});
</script>
</body>
</html>
Native Web Component
Ein Build-Script erzeugt eine eigenständige <sumaq-editor>-Komponente ohne externe Abhängigkeiten. Das Script liegt im Editor-Repository unter adapters/.
./adapters/build-web-component.sh ./path/to/your/project/
Das Ergebnis ist eine einzige Datei. Einbinden, Tag setzen, fertig. Attribute steuern die Konfiguration: toolbar (kommagetrennt), content (initialer HTML-Inhalt).
<script src="sumaq-editor.js"></script>
<sumaq-editor
toolbar="h2,h3,p,|,bold,italic,|,link"
content="<p>Hier schreiben</p>">
</sumaq-editor>
Events funktionieren wie bei jedem anderen HTML-Element:
document.querySelector('sumaq-editor')
.addEventListener('sumaq:change', (e) => {
console.log(e.detail.html);
});
Die Methoden .getHTML(), .getText() und .setHTML() stehen direkt auf dem Element zur Verfügung:
const el = document.querySelector('sumaq-editor');
// Inhalt lesen
el.getHTML();
el.getText();
// Inhalt setzen
el.setHTML('<p>Neuer Inhalt</p>');
Über die .editor-Property erreicht ihr die vollständige Sumaq-Instanz mit allen Methoden und Events, die in der API-Referenz dokumentiert sind:
const el = document.querySelector('sumaq-editor');
// Auf Events reagieren
el.editor.on('input', (html) => {
console.log('Inhalt geändert:', html);
});
// Theme wechseln
el.editor.setTheme('elegant');
PHP-CMS
Euer Server rendert den Container mit bestehendem Inhalt. JavaScript bindet Sumaq beim Laden ein. Beim Absenden holt getHTML() die bereinigte Ausgabe in ein verstecktes Input-Feld.
<form method="post">
<div id="editor"><?= $content ?></div>
<input type="hidden" name="content" id="content-field">
<button type="submit">Speichern</button>
</form>
<script type="module">
import Sumaq from '/assets/js/sumaq.js';
const editor = new Sumaq('#editor', {
toolbar: ['h2', 'h3', 'p', '|', 'bold', 'italic', '|', 'link', 'image']
});
document.querySelector('form').addEventListener('submit', () => {
document.getElementById('content-field').value = editor.getHTML();
});
</script>
React
Ein Build-Script erzeugt eine eigenständige SumaqEditor-Komponente für React: der gesamte Editor-Code wird dabei in die Datei eingebettet.
./adapters/build-react.sh ./path/to/your/project/
Das Ergebnis ist eine einzelne JSX-Datei. Importieren, Props setzen, fertig:
import SumaqEditor from './SumaqEditor';
function App() {
return (
<SumaqEditor
toolbar={['h2', 'h3', 'p', '|', 'bold', 'italic', '|', 'link']}
content="<p>Initialer Inhalt</p>"
onChange={(html) => console.log(html)}
onInput={(html) => console.log(html)}
/>
);
}
Props: toolbar (Array), content (initialer HTML-Inhalt), onChange, onInput, styleOverrides (CSS-String für Shadow DOM), extensions (Array).
Über eine Ref stehen .editor (Sumaq-Instanz), .getHTML(), .getText() und .setHTML() zur Verfügung:
const ref = useRef();
<SumaqEditor ref={ref} toolbar={['h2', 'h3', 'p']} />
// Später:
ref.current.getHTML();
ref.current.setHTML('<p>Neuer Inhalt</p>');
Vue
Ein Build-Script erzeugt eine eigenständige SumaqEditor-Komponente für Vue 3: Composition API, <script setup>.
./adapters/build-vue.sh ./path/to/your/project/
Das Ergebnis ist eine einzelne .vue-Datei:
<script setup>
import SumaqEditor from './SumaqEditor.vue';
function onEditorChange(html) {
console.log(html);
}
</script>
<template>
<SumaqEditor
:toolbar="['h2', 'h3', 'p', '|', 'bold', 'italic', '|', 'link']"
content="<p>Initialer Inhalt</p>"
@change="onEditorChange"
@input="(html) => console.log(html)"
/>
</template>
Props: toolbar (Array), content (String), style-overrides (CSS-String), extensions (Array). Events: change, input.
Über eine Template-Ref stehen .editor, .getHTML(), .getText() und .setHTML() zur Verfügung:
<template>
<SumaqEditor ref="editorRef" :toolbar="['h2', 'h3', 'p']" />
</template>
<script setup>
import { ref } from 'vue';
const editorRef = ref(null);
// Später:
editorRef.value.getHTML();
editorRef.value.setHTML('<p>Neuer Inhalt</p>');
</script>
Svelte
Ein Build-Script erzeugt eine eigenständige SumaqEditor-Komponente für Svelte.
./adapters/build-svelte.sh ./path/to/your/project/
Das Ergebnis ist eine einzelne .svelte-Datei:
<script>
import SumaqEditor from './SumaqEditor.svelte';
</script>
<SumaqEditor
toolbar={['h2', 'h3', 'p', '|', 'bold', 'italic', '|', 'link']}
content="<p>Initialer Inhalt</p>"
on:change={(e) => console.log(e.detail)}
on:input={(e) => console.log(e.detail)}
/>
Props: toolbar (Array), content (String), styleOverrides (CSS-String), extensions (Array). Events: change, input (Payload in e.detail).
Über bind:this stehen .getEditor(), .getHTML(), .getText() und .setHTML() zur Verfügung:
<script>
import SumaqEditor from './SumaqEditor.svelte';
let editorComponent;
</script>
<SumaqEditor bind:this={editorComponent} toolbar={['h2', 'h3', 'p']} />
<button on:click={() => console.log(editorComponent.getHTML())}>
HTML ausgeben
</button>
Performance
Bis 5.000 Absätze (~25.000 DOM-Knoten) bleibt jede Operation innerhalb des 16ms-Frame-Budgets. Bei 10.000 Absätzen bleibt Tippen mit 18ms flüssig. Formatierungsoperationen werden spürbar. Ab 15.000 Absätzen überschreitet die Eingabelatenz die wahrnehmbare Schwelle.
getHTML() bleibt bei jeder Größe schnell: 50.000 Absätze werden in 30ms zurückgelesen.
Ergebnisse eines Stresstests, gemessen in Chromium, mit schrittweise größeren Dokumenten in einer einzelnen Sumaq-Instanz. „Tippen“ ist execCommand('insertText'), „Fett“ ist execCommand('bold') auf eine Selektion, „getHTML()“ liest das gesamte Dokument zurück.
| Absätze | DOM-Knoten | Tippen | Fett | getHTML() |
|---|---|---|---|---|
| 500 | 3.800 | <1ms | <1ms | <1ms |
| 1.000 | 7.800 | <1ms | <1ms | 1ms |
| 2.000 | 9.400 | 3,5ms | 10ms | 2ms |
| 3.000 | 15.000 | 8ms | 10ms | 3ms |
| 5.000 | 25.000 | 10ms | 21ms | 6ms |
| 10.000 | 50.000 | 18ms | 58ms | 9ms |
| 20.000 | 100.000 | 43ms | 108ms | 11ms |
| 50.000 | 250.000 | 106ms | 439ms | 30ms |
Der Engpass bei hohen Knotenzahlen ist execCommand und der DOM-Mutationsaufwand des Browsers selbst, nicht Sumaq. Kein Virtual-DOM-Diffing, kein State-Reconciliation-Overhead, nur der Browser, der tut, was Browser tun.
Warum Shadow DOM
Das Problem
Sumaq ist dafür gebaut, in jede Seite eingebettet zu werden: CMS-Backends, Kunden-Websites, Admin-Panels. Jede Host-Seite bringt eigenes CSS mit: Resets, Framework-Styles, Komponentenbibliotheken, eigene Overrides. Ohne Kapselung dringen diese Styles in den Editor ein und zerstören seine Darstellung.
Beispiele, was ohne Isolation schiefgeht:
- Ein globales
button { background: red; }überschreibt alle Toolbar-Buttons - Ein CSS-Reset entfernt Listenpunkte von
<ul>/<ol>im Inhaltsbereich - Framework-Styles (Bootstrap, Tailwind, etc.) überschreiben Überschriftengrößen, Link-Farben, Tabellenrahmen
* { box-sizing: border-box; }oder* { margin: 0; }können je nach Host kollidieren oder nicht, unberechenbar
Der Editor kann nicht kontrollieren oder vorhersagen, welches CSS die Host-Seite lädt.
Die Lösung
Einen Shadow-Root an ein Host-Element anhängen und den gesamten Editor darin rendern. Das gesamte CSS ist als <style>-Element innerhalb des Shadow-Root eingebettet, mit dem Ergebnis:
- Vollständige CSS-Kapselung: Host-Styles können die Shadow-Grenze nicht überqueren
- Host-Seiten-Resets, Frameworks und globale Regeln haben keinerlei Auswirkung auf den Editor
- Die eigenen Styles des Editors haben keinerlei Auswirkung auf die Host-Seite
- Typografie-Basis für den Inhaltsbereich ist eigenständig und vorhersagbar
- Keine Spezifitätsspiele, kein
!important, kein Kampf gegen die Kaskade
Implementierung
Architektur
container (Light DOM)
└── shadowHost <div>
└── #shadow-root (open)
├── <style>/* gesamtes Editor-CSS */</style>
└── .sumaq-editor
├── .sumaq-toolbar
├── .sumaq-content [contentEditable]
├── .sumaq-source <textarea>
├── .sumaq-modal
└── .sumaq-modal (link)
Zentrale Entscheidungen
- Offener Shadow-Root (
mode: 'open'): erlaubt externem JS den Zugriff auf das Shadow DOM viashadowRootbei Bedarf (z.B. für Tests, programmatischen Zugriff auf den Inhaltsbereich) - Zwischengeschaltetes Host-Element: ein einfaches
<div>sitzt zwischen Container und Shadow-Root, damitdestroy()es sauber entfernen und den Original-Container-Inhalt wiederherstellen kann - Gesamtes CSS eingebettet: die externe
sumaq.css-Datei wird als Dokumentation/Fallback behalten, aber die Shadow-DOM-Version trägt alle Styles intern viastatic styles - Typografie-Basis: da keine Host-Styles den Inhaltsbereich erreichen, muss der Editor seine eigene vollständige typografische Grundlage definieren: Überschriften, Absätze, Listen (einschließlich verschachtelter Marker-Progression), Zitate, Inline-Formatierung, Tabellen, Codeblöcke und Links
Was weiterhin durch Shadow DOM funktioniert
document.execCommand(): operiert auf dem fokussiertencontentEditableunabhängig von der Shadow-Grenzewindow.getSelection(): gibt Selektionen innerhalb offener Shadow-Roots in allen modernen Browsern zurückaria-pressed,role="separator", SVG<title>: der Barrierefreiheitsbaum ist vollständig durch Shadow DOM exponiertposition: fixedauf Modalen: positioniert weiterhin relativ zum Viewportselectionchange-Event aufdocument: feuert weiterhin für Selektionen innerhalb des Shadow-Root
Was angepasst werden musste
document.activeElement: gibt den Shadow-Host zurück, nicht das fokussierte Element darin. Derselectionchange-Handler prüft stattdessencontent.contains(selection.anchorNode)destroy(): entfernt das Shadow-Host-Element statt des Wrappers direktselectionchange-Listener-Bereinigung: als benannte Referenz (this._onSelectionChange) gespeichert, damit er beim Destroy sauber entfernt werden kann
Kompromisse
- Dateigröße: gesamtes CSS als String in der JS-Datei dupliziert (~3KB). Akzeptabel für eine eigenständige Komponente
- Debugging: Styles leben im
<style>-Element des Shadow-Root statt in einer separaten Datei. Browser-DevTools handhaben das gut (Shadow-Root inspizieren, berechnete Styles sehen) - Kein externes Theming: Host-Seiten-CSS kann den Editor nicht stylen. Das ist der ganze Sinn, bedeutet aber, dass Anpassung über die
static styles-Eigenschaft oder eine zukünftige Theming-API (CSS Custom Properties, die Shadow-Grenzen durchdringen) erfolgen muss
Browser-Support
Shadow DOM v1 wird von allen modernen Browsern seit 2020 unterstützt. Kein Polyfill nötig.
| Browser | Seit |
|---|---|
| Chrome | 53 (2016) |
| Firefox | 63 (2018) |
| Safari | 10 (2016) |
| Edge | 79 (2020, Chromium) |
| Chrome Android | 53 (2016) |
| Safari iOS | 10 (2016) |
| Samsung Internet | 6.0 (2017) |