← Gesamte Dokumentation
Werkzeuge für Autoren

Fußnoten, Format übertragen & Groß-/Kleinschreibung

Drei Werkzeuge für lange Dokumente, die andere kommerzielle Editoren hinter einen kostenpflichtigen Tarif stellen. Fußnoten mit Markierungen und Anmerkungen, die sich selbst neu nummerieren, Format übertragen, das die Zeichenformatierung von einer Auswahl auf eine andere kopiert, und Groß-/Kleinschreibung ändern für die Auswahl. Alle drei sind in der Standardlizenz enthalten.

Inhaltsverzeichnis

Fügt einen Inhaltsblock ein, der aus den Überschriften des Dokuments gebildet wird, mit Links, die zu ihnen springen, und hält ihn beim Schreiben aktuell. Benennen Sie eine Überschrift um, fügen Sie eine hinzu, löschen Sie eine, verschieben Sie einen Abschnitt – die Liste leitet sich neu aus dem Dokument ab und kann daher nicht veralten.

Das ist nicht dasselbe wie der Bereich Dokumentgliederung. Dieser Bereich ist Oberfläche des Editors zur Navigation und erscheint nie in Ihrem gespeicherten HTML. Ein Inhaltsverzeichnis ist Teil des Dokuments, das Sie veröffentlichen oder drucken, daher übersteht es getHTMLCode() und liest sich auch ohne laufendes JavaScript richtig. Warum eine falsch gezogene Grenze hier entweder einen veralteten Inhaltsblock oder ein unbrauchbares Panel ergibt, beschreibt ein Inhaltsblock ist Inhalt, ein Gliederungsbereich nicht.

editor.insertTableOfContents();   // one per document
editor.updateTableOfContents();   // force a rebuild (normally automatic)
editor.hasTableOfContents();      // -> true | false
editor.removeTableOfContents();
editor.getTableOfContents();
// -> [{ id: "rte-h-3k1a0", level: 1, text: "Master Agreement", page: 1 }, ...]
KonfigurationStandardwertWas sie tut
tocTitle"Contents"Überschrift über der Liste. Auf "" setzen, um sie wegzulassen.
tocMinLevel / tocMaxLevel1 / 3Einbezogene Überschriftenebenen, einschließlich der Grenzen. Der Einzug folgt der Ebene.
tocPageNumbers"auto""auto" zeigt Seitenzahlen, wenn die Seitenansicht eingeschaltet ist, true versucht es immer, false nie.

Seitenzahlen. Ist das Plugin für den Seitenumbruch geladen und die Seitenansicht eingeschaltet, erhält jeder Eintrag die Seite, auf der seine Überschrift steht, mit einer Punktlinie wie in einem gedruckten Inhaltsverzeichnis. Die Zahlen stammen von denselben Seitengrenzen, die das Overlay der Seitenansicht zeichnet, und stimmen daher immer mit dem überein, was Sie sehen. Außerhalb der Seitenansicht wird keine neue Zahl berechnet – eine Seitenzahl wäre dort eine Fiktion –, aber bereits im Block stehende Zahlen werden beibehalten, sodass das Laden eines in der Seitenansicht gespeicherten Dokuments sie nie stillschweigend entfernt. Wechseln Sie erneut in die Seitenansicht, um sie zu aktualisieren.

Der Block ist contenteditable="false" und wird aus den Überschriften neu erzeugt, ist also nicht von Hand bearbeitbar – löschen Sie ihn als Einheit und fügen Sie ihn neu ein, um ihn zu verschieben. Überschriften in erzeugten Bereichen (dem Inhaltsblock selbst, dem Fußnotenabschnitt) werden übersprungen, sodass er nie seinen eigenen Titel aufführt.

Fußnoten

Fügt am Cursor eine nummerierte Verweismarkierung ein und eine passende Anmerkung in einen Fußnotenabschnitt am Ende des Dokuments und hält beide synchron. Die Nummerierung folgt der Reihenfolge im Dokument: Fügen Sie über einer vorhandenen Fußnote eine neue ein, löschen Sie eine Markierung oder verschieben Sie einen Absatz, und jede Markierung und Anmerkung wird neu nummeriert und die Liste der Anmerkungen passend umsortiert. Das Löschen einer Markierung löscht ihre Anmerkung, und das Entfernen der letzten Markierung entfernt den ganzen Abschnitt – im gespeicherten HTML bleiben keine Waisen zurück.

<script src="richtexteditor/rte.js"></script>
<script src="richtexteditor/plugins/all_plugins.js"></script>

<script>
  var editor = new RichTextEditor("#editor");

  editor.insertFootnote();                 // marker at the caret, caret moves into the note
  editor.insertFootnote("Ibid., p. 42");   // ...or supply the note text up front

  editor.getFootnotes();
  // -> [{ id: "fn3k1a0", number: 1, text: "Ibid., p. 42" }]

  editor.syncFootnotes();      // force a renumber (normally automatic)
  editor.getFootnotesCss();    // the stylesheet, to reuse on your public page
</script>
KonfigurationStandardwertWas sie tut
footnotesTitle"Footnotes"Überschrift über dem Abschnitt der Anmerkungen. Auf "" setzen, um sie wegzulassen.
footnotesNumbering"decimal"Listenstil der Anmerkungen: decimal, lower-roman, upper-roman, lower-alpha, upper-alpha.
footnotesMarkerPrefix""Umschließt die Nummer der Markierung, z. B. "[" für [1].
footnotesMarkerSuffix""Die schließende Hälfte dazu, z. B. "]".

Was in Ihrem HTML landet

Fußnoten sind Inhalt und sollen daher anders als das Overlay der Seitenansicht in getHTMLCode() erhalten bleiben. Die Nummer wird als echter Text geschrieben statt über einen CSS-Counter gezeichnet, sodass das gespeicherte HTML in sich vollständig ist: Veröffentlichen Sie es, exportieren Sie es oder stellen Sie es auf einer Seite dar, die unser Stylesheet nie lädt, und die Nummerierung ist trotzdem da und stimmt.

<p>The clause was struck out<sup class="rte-fn-ref" data-fn-id="fn3k1a0"
     data-fn-number="1" id="fnref-fn3k1a0" contenteditable="false">1</sup>.</p>

<section class="rte-footnotes">
  <h2 class="rte-fn-title">Footnotes</h2>
  <ol class="rte-fn-list" data-numbering="decimal">
    <li class="rte-fn-note" data-fn-id="fn3k1a0" data-fn-number="1" id="fn-fn3k1a0">Ibid., p. 42<a
      class="rte-fn-back" href="#fnref-fn3k1a0" contenteditable="false">&#8617;</a></li>
  </ol>
</section>

Markierungen sind contenteditable="false", sodass der Cursor sie als Einheit behandelt und nicht in der Nummer landen kann. Ein Klick auf eine Markierung springt zu ihrer Anmerkung; ein Klick auf ↩ springt zurück.

Format übertragen

Nimmt die Zeichenformatierung am Cursor auf und überträgt sie auf die nächste Auswahl. Die Formatierung wird aus dem berechneten Style gelesen, daher spielt es keine Rolle, ob die Quelle <b>, style="font-weight:700" oder eine CSS-Klasse war. Beim Übertragen wird außerdem die Zeichenformatierung entfernt, die schon im Ziel steckt – erst dadurch funktioniert es, unformatierten Text auf fetten Text zu übertragen.

editor.copyFormatting();       // capture at the caret
editor.pasteFormatting();      // apply to the current selection

editor.toggleFormatPainter();      // one-shot: capture, then paint the next selection
editor.toggleFormatPainter(true);  // sticky: keep painting until cancelled
editor.cancelFormatPainter();      // or press Escape
editor.isFormatPainterActive();    // -> true | false
editor.getCapturedFormatting();    // -> { "font-weight": "700", "color": "rgb(204, 0, 0)" }

Übertragen werden nur Eigenschaften, die sich tatsächlich von der Grundeinstellung des Editors unterscheiden, sodass ein Übertragen ein kurzes style-Attribut erzeugt statt einer Wand geerbter Standardwerte. Erneutes Übertragen auf denselben Abschnitt verschachtelt keine Wrapper. Welche Eigenschaften übertragen werden, lässt sich über formatPainterProperties konfigurieren; Eigenschaften auf Blockebene wie Ausrichtung und Ränder sind absichtlich ausgenommen, denn ein Werkzeug, das das Dokument neu umbricht, ist nicht der Zweck.

Groß-/Kleinschreibung ändern

Schreibt die tatsächlichen Zeichen der Auswahl um – kein text-transform, das nur so lange richtig aussieht, bis das HTML anderswo verwendet wird. Inline-Markup bleibt erhalten: Ändern Sie die Schreibung von the <strong>Data</strong> Act, und das <strong> ist danach noch da. Ohne Auswahl wirkt die Funktion auf das Wort unter dem Cursor.

editor.changeCase("upper");      // THE DATA ACT
editor.changeCase("lower");      // the data act
editor.changeCase("title");      // The Data Act
editor.changeCase("sentence");   // The data act
editor.changeCase("toggle");     // tHE dATA aCT

editor.getChangeCaseModes();     // -> ["upper","lower","title","sentence","toggle"]

Title Case lässt kurze Wörter (a, an, and, of, the, to …) klein, sofern sie nicht am Anfang oder Ende stehen, und wendet dieselbe Regel innerhalb von Bindestrich-Zusammensetzungen an – so wird state-of-the-art zu State-of-the-Art, nicht zu State-Of-The-Art. Die Liste bearbeiten Sie mit titleCaseSmallWords.

Autokorrektur & Typografie

Korrigiert häufige Tippfehler während des Tippens, schreibt Satzanfänge groß und macht aus getippten Näherungen die richtigen Zeichen: Gerade Anführungszeichen werden zu typografischen, -- wird zum Geviertstrich, ... zur Ellipse, (c) zu © und 1/2 zu ½.

editor.setAutocorrectEnabled(false);   // off at runtime
editor.isAutocorrectEnabled();

editor.addAutocorrectRule("acheive", "achieve");
editor.getAutocorrectRules();

// Run the same typography normalisation over imported or pasted text:
editor.applyTypography('He said "hello" -- it is 1/2 done...');
// -> 'He said “hello” — it is ½ done…'
KonfigurationStandardwertWas sie tut
autocorrectEnabledtrueHauptschalter.
autocorrectSpellingtrueTabelle häufiger Tippfehler (teh → the).
autocorrectCapitalizetrueDas erste Wort eines Satzes großschreiben.
autocorrectTypographytrueTypografische Anführungszeichen, Striche, Ellipse, Pfeile, Symbole, Brüche.
autocorrectReplacementsnullZusätzliche Regeln, die über die Standardregeln gelegt werden. Ordnen Sie ein Wort sich selbst zu, um eine eingebaute Regel abzuschalten.

In Code greift nichts. <code>, <pre>, <kbd>, <samp> und <var> werden vollständig übersprungen, ebenso jeder Teilbaum mit data-rte-no-autocorrect. Typografische Anführungszeichen in einem Codebeispiel beschädigen es, und das ist das Schädlichste, was eine solche Funktion tun kann.

Drücken Sie direkt nach einer Korrektur die Rücktaste, um sie rückgängig zu machen, und behalten Sie genau das, was Sie getippt haben – derselbe Ausweg wie in Word und Google Docs. Die eingebaute Wortliste ist absichtlich zurückhaltend: Zeichenfolgen, die auch echte Wörter sind (ill, lets, its), bleiben unverändert, weil ein falscher Treffer bei einem echten Wort schlimmer ist als eine verpasste Korrektur.

Seriendruckfelder

Platzhalter, die für Daten stehen, sodass ein Dokument zu einer Vorlage wird, die Sie mit vielen Datensätzen rendern – Serienbriefe, Rechnungsläufe, persönliche Anschreiben, generierte Verträge. Jedes Feld ist ein atomarer Chip mit contenteditable="false", sodass es nie halb zu {{customer.na}} gelöscht werden und stillschweigend nicht mehr aufgelöst werden kann.

var editor = new RichTextEditor("#editor", {
  mergeFields: [
    { id: "recipient.name",  label: "Recipient name", sample: "Ada Lovelace" },
    { id: "invoice.total",   label: "Invoice total",  sample: "£1,240.00" },
    { id: "date.today",      label: "Today's date",   sample: function () { return new Date().toLocaleDateString(); } }
  ]
});

editor.insertMergeField("recipient.name");
editor.listMergeFields();
// -> [{ id: "recipient.name", label: "Recipient name", known: true, count: 1 }]

editor.setMergeFieldPreview(true);    // show sample data instead of labels
editor.toggleMergeFieldPreview();
editor.isMergeFieldPreview();

// Render one record. Flat or nested data both work.
editor.renderMergeFields({ "recipient.name": "Grace Hopper", invoice: { total: "£90" } });
// -> HTML with the values substituted. The document itself is untouched.
KonfigurationStandardwertWas sie tut
mergeFieldsBeispielsatzDefinitionen { id, label, sample }. Die mitgelieferte Liste ist ein Beispiel – ersetzen Sie sie durch Ihr eigenes Schema.
mergeFieldPrefix / Suffix"{{" / "}}"Zeichen um die Beschriftung, damit ein Platzhalter sofort erkennbar ist.
mergeFieldMissing"placeholder"Was renderMergeFields tut, wenn die Daten keinen Wert enthalten: den Platzhalter sichtbar lassen oder ihn mit "empty" entfernen.

Die Vorschau gelangt nie in Ihr gespeichertes HTML. Das ist die eine wirklich gefährliche Stelle dieser Funktion: Würden das Einschalten der Vorschau und ein Klick auf Speichern „Ada Lovelace“ dort in die Vorlage schreiben, wo vorher {{Recipient name}} stand, würde der Benutzer seine Vorlage schon durch bloßes Ansehen zerstören. Deshalb sind die Serialisierer umhüllt – getHTMLCode, getJSON, getHTMLContent und getText setzen für die Dauer des Aufrufs jeden Chip auf seine Beschriftung zurück. Speichern Sie während der Vorschau, erhalten Sie trotzdem die Vorlage zurück.

Nach demselben Prinzip arbeitet renderMergeFields() auf einem losgelösten Klon: Das Erzeugen der zusammengeführten Ausgabe verändert nie das Dokument, das gerade bearbeitet wird. Ein Feld ohne passende Daten behält seinen Platzhalter, statt leer zu erscheinen, weil eine stillschweigend leere Rechnungszeile schlimmer ist als eine erkennbar nicht ausgefüllte. Felder, die aus einer anderswo erstellten Vorlage eingefügt wurden, werden mit known: false gemeldet statt verworfen, sodass Sie dem Benutzer sagen können, was seine Daten liefern müssen.

Tabellen sortieren & Zeilen nummerieren

Sortieren Sie eine Tabelle nach der Spalte, in der der Cursor steht, und fügen Sie eine automatische Nummernspalte hinzu, die sich nach jedem Sortieren neu nummeriert.

editor.sortTableAtCaret("asc");        // or "desc"
editor.sortTableByColumn(2, "desc");
// -> { ok: true, sortedBy: 2, direction: "desc", detectedType: "number" }

editor.canSortTable();                 // -> { ok: false, reason: "...merged cells..." }

editor.addTableRowNumbers({ series: "upper-roman" });
editor.removeTableRowNumbers();
editor.hasTableRowNumbers();

Der Spaltentyp wird erkannt, nicht angenommen. Eine Spalte mit Preisen wird numerisch sortiert, auch wenn in den Zellen $1,240.00 oder (500) (Buchhaltungsschreibweise für einen negativen Wert) steht, und eine Spalte mit Datumsangaben chronologisch statt alphabetisch. Eine numerische Spalte als Text zu sortieren, ist der klassische Fehler, der 10 vor 9 setzt. Leere Zellen wandern in beiden Richtungen nach unten, weil ein leeres Feld „kein Wert“ bedeutet, nicht „der kleinste Wert“. Die Sortierung ist stabil, sodass das Sortieren nach einer zweiten Spalte die Reihenfolge der ersten erhält.

Bei Tabellen mit verbundenen Zellen verweigert die Funktion das Sortieren und sagt, warum, statt sie schlecht zu sortieren. Ein rowspan verliert seine Bedeutung, sobald sich die Zeilen verschieben; eine Tabelle, an der jemand eine Stunde gearbeitet hat, stillschweigend zu zerstören, ist weit schlimmer, als das Sortieren abzulehnen. <thead> und <tfoot> bewegen sich nie, und eine erste Zeile nur aus <th> wird auch ohne <thead> als Kopfzeile behandelt. Die Zeilennummerierung richtet sich nach der Position, nach einem Sortieren ist Zeile 1 also das, was jetzt oben steht.

Dauerstift

Schalten Sie ein festes Zeichenformat ein, und alles, was Sie danach tippen, erhält es – wie ein Marker, mit dem Sie ein Dokument in einer Farbe kommentieren, die offensichtlich nicht der Originaltext ist. Anders als bei Format übertragen ist die Ausgabe des Stifts echter Inhalt und bleibt im gespeicherten HTML erhalten.

editor.setPermanentPen(true);
editor.togglePermanentPen();
editor.isPermanentPenActive();

editor.setPermanentPenStyle({ "color": "#0b6", "font-style": "italic" });
editor.getPermanentPenStyle();

Ein ganzer Strich landet in einem Span, den der Stift immer weiter verlängert, statt in einem Span pro Tastenanschlag. Wird der Cursor bewegt, endet der Strich, sodass der nächste Abschnitt einen eigenen Span erhält, statt dass Text in den vorherigen springt. Beim Ausschalten des Stifts tritt der Cursor aus dem Abschnitt heraus – sonst ginge das Tippen innerhalb des gestalteten Spans weiter, und der Stift schiene das Ausschalten zu ignorieren.

Querverweise

„Siehe Klausel 3.2“, „Abbildung 4“, „beschrieben in Leistungsumfang“ – Verweise, die weiterhin auf das Richtige zeigen, nachdem das Dokument drumherum bearbeitet wurde. Dieses Teil hält die übrigen Dokumentwerkzeuge zusammen: Die juristische Nummerierung nummeriert Klauseln neu, und der Inhaltsblock leitet sich neu ab, aber ein Satz wie „siehe Klausel 3.2“ bleibt eingefroren, solange ihn nichts aktualisiert. In einem Vertrag ändert ein veralteter Verweis, was das Dokument aussagt.

editor.listCrossReferenceTargets();
// -> [{ id, type: "heading"|"clause"|"footnote"|"table"|"figure", label, number }]

editor.insertCrossReference(targetId, "label");   // "clause 3.2" / "Table 2"
editor.insertCrossReference(targetId, "text");    // the heading's own text
editor.insertCrossReference(targetId, "number");  // "3.2"
editor.insertCrossReference(targetId, "page");    // page number, in page view
editor.insertCrossReference(targetId, "position");// "above" / "below"

editor.updateCrossReferences();
editor.getCrossReferences();

Klauselnummern werden aus der Position in der Liste abgeleitet, genauso wie die CSS-Counter, die sie darstellen, sodass das Einfügen einer Klausel darüber jeden Verweis darunter neu nummeriert. Verweise auf Überschriften folgen einer umbenannten Überschrift. Tabellen und Abbildungen werden automatisch nummeriert oder verwenden ihr <caption> / <figcaption>, sofern vorhanden.

Ein Verweis, dessen Ziel gelöscht wurde, schlägt sichtbar fehl – er zeigt [reference not found] in Rot an, statt seinen letzten Wert zu behalten. Word tut dasselbe, und aus demselben Grund: Eine plausible, aber falsche Klauselnummer ist weit gefährlicher als eine erkennbar defekte. Der angezeigte Text ist echter Text im gespeicherten HTML, sodass der Verweis auch außerhalb des Editors richtig zu lesen ist.

Zeilennummern am Rand

Nummeriert jede Zeile am linken Rand, so wie ein Textverarbeitungsprogramm es für Schriftsätze, Gesetzestexte, Protokolle und alles tut, was zeilengenau zitiert wird („Seite 4, Zeilen 12–15“). Die Vorschriften für Gerichtseinreichungen in mehreren Rechtsordnungen verlangen nummerierte Zeilen, deshalb haben Dokumenteditoren diese Funktion und Browser-Editoren in der Regel nicht.

editor.setLineNumbers(true);
editor.toggleLineNumbers();
editor.isLineNumbers();
editor.getLineCount();          // visual lines, not blocks

editor.setLineNumberOptions({
  restart:  "page",   // "continuous" | "page" (needs page view) | "block"
  interval: 5,        // show every 5th number
  start:    1,
  gutter:   38        // gutter width in px
});

Gezählt werden sichtbare Zeilen, nicht Absätze. Ein Absatz, der über vier Zeilen umbricht, erhält vier Nummern, weil „Zeile 12“ für den Zitierenden genau das bedeutet. Die Zeilenboxen werden mit Range.getClientRects() ermittelt, sodass die Zählung dem tatsächlich dargestellten Layout folgt – ändern Sie die Fensterbreite, und die Nummerierung fließt mit dem Text mit.

Mit restart: "page" und eingeschalteter Seitenansicht beginnt die Nummerierung oben auf jeder Seite neu, wie es die Einreichungsvorschriften festlegen. Wie das Overlay der Seitenansicht selbst ist der Rand rein darstellend: Er ist contenteditable="false" und wird bei jedem Serialisieren entfernt, sodass das gespeicherte HTML Byte für Byte identisch ist, ob Zeilennummern ein- oder ausgeschaltet sind – einschließlich des Innenabstands für den Rand, der sonst als Inline-Style auf den bearbeitbaren Bereich durchsickern würde.

Formatierungszeichen

Die Schaltfläche aus Word: Absatzmarken, Blockumrisse und – der wirklich nützliche Teil – eine Hervorhebung von Zeichen, die unsichtbar, aber nicht harmlos sind. Geschützte Leerzeichen, Leerzeichen ohne Breite, bedingte Trennstriche und Bidi-Markierungen kommen alle mit eingefügten Inhalten herein und verursachen Layoutfehler, die niemand sehen kann.

editor.setFormattingMarks(true);
editor.toggleFormattingMarks();
editor.isFormattingMarks();

// config
formattingMarkPilcrow:    true,   // ¶ at the end of each block
formattingMarkBlocks:     true,   // dashed outline around block elements
formattingMarkInvisibles: true    // nbsp / zero-width / soft hyphen / bidi

Absatzmarken und Umrisse werden vollständig in CSS gezeichnet und können das Dokument daher unter keinen Umständen verändern. Unsichtbare Zeichen sind per CSS nicht erreichbar und erhalten deshalb einen Wrapper – aber jeder Wrapper wird beim Serialisieren entfernt und beim Abschalten der Funktion beseitigt, und wiederhergestellt wird das ursprüngliche Zeichen, nicht das Anzeigesymbol. Innerhalb von <code> oder <pre>, wo das Zeichen beabsichtigt sein kann, werden nie Markierungen angewendet.

Fairerweise: Hier herrscht Gleichstand, kein Vorteil. TinyMCE liefert visualchars und visualblocks im kostenlosen Kern mit, und CKEditor hat show-blocks.

Wasserzeichen

Zeichnet DRAFT, CONFIDENTIAL oder einen Kundennamen hinter den Inhalt, sodass ein Leser auf einen Blick erkennt, dass er das Dokument nicht als endgültig behandeln oder nicht weitergeben soll.

editor.setWatermark(true);
editor.toggleWatermark();
editor.isWatermark();

editor.setWatermarkOptions({
  text:     "CONFIDENTIAL",
  mode:     "tile",     // "tile" | "single"
  opacity:  0.18,
  angle:    -30,
  fontSize: 48,
  color:    "#94a3b8",
  print:    false,      // opt in to include it when printing / exporting PDF
  image:    null        // data URI or URL; overrides the text
});

Es ist nie Teil Ihres Inhalts. Das Zeichen wird als SVG-Hintergrund dargestellt, kann daher weder ausgewählt noch kopiert noch per Suchen und Ersetzen gefunden werden und wird bei jedem Serialisieren entfernt. Das ist mehr als Ordnungsliebe: Ein in das Dokument geschriebenes Wasserzeichen würde in einer veröffentlichten Seite weiterleben, lange nachdem der Entwurf kein Entwurf mehr ist, und „CONFIDENTIAL“ als Inhalt lässt sich mühelos löschen, ein Zeichen in der Darstellungsschicht dagegen nicht.

Das Drucken ist eine eigene Entscheidung, unabhängig von der Markierung am Bildschirm, daher wird das Wasserzeichen standardmäßig nicht gedruckt. Setzen Sie print: true, um es in die Druckausgabe und den PDF-Export zu übernehmen.

Rechts nach links & bidirektionaler Text

Arabisch, Hebräisch, Persisch und Urdu laufen von rechts nach links. Die Richtung lässt sich pro Block oder für das ganze Dokument setzen, aus dem Inhalt erkennen oder mit dir="auto" dem Browser überlassen.

editor.setTextDirection("rtl");     // the block(s) in the selection
editor.toggleTextDirection();
editor.getTextDirection();          // -> "rtl" | "ltr"

editor.setBaseDirection("rtl");     // the whole document
editor.getBaseDirection();

editor.detectTextDirection("123 שלום");   // -> "rtl"  (first-strong)
editor.autoDetectTextDirection();          // stamp every block from its own text

// Mixed content: isolate a run so the surrounding direction cannot reorder it
editor.isolateBidi("https://example.com", "ltr");   // -> <bdi dir="ltr">…</bdi>
editor.insertIsolated("https://example.com", "ltr");

Die Richtung ist Inhalt. Anders als das Overlay der Seitenansicht oder das Wasserzeichen wird dir in das Dokument geschrieben und bleibt in getHTMLCode() erhalten – ein Dokument, dessen Richtung beim Speichern verschwände, wäre unlesbar.

Die Erkennung verwendet die First-Strong-Regel, denselben Algorithmus, den die HTML-Spezifikation für dir="auto" festlegt: Ein Absatz übernimmt die Richtung seines ersten stark gerichteten Zeichens, Ziffern und Satzzeichen werden übersprungen. Deshalb läuft "123 שלום" von rechts nach links, "123 hello" dagegen von links nach rechts, und deshalb bleibt ein Absatz nur aus Zahlen unverändert, statt dass geraten wird.

Ein Richtungswechsel tauscht eine explizite Ausrichtung. Das Setzen von dir ändert nur die Standardausrichtung, sodass ein Absatz mit explizitem text-align:left in einem RTL-Block am linken Rand kleben bliebe und kaputt aussähe. Eine explizite physische Ausrichtung wird gespiegelt und ganz entfernt, wenn sie auf der natürlichen Seite der neuen Richtung landet, sodass der Block von da an dir folgt.

Gemischte Inhalte werden isoliert, nicht nur markiert. Wird eine LTR-URL ohne Isolation in einen RTL-Satz eingefügt, ordnet der Bidi-Algorithmus die umgebenden Satzzeichen um – so erscheinen bei (https://example.com) die Klammern auf den falschen Seiten. insertIsolated() umschließt den Abschnitt mit <bdi>, damit das nicht passieren kann.

Listen, Blockzitate und Tabellen werden per CSS gespiegelt, sodass Aufzählungszeichen und Zitatbalken am richtigen Rand stehen.

Auch der Editor selbst wird gespiegelt. Die Richtung des Dokuments ist nur die halbe Arbeit – ein Autor auf Arabisch oder Hebräisch sollte keinen Text von rechts nach links in einer Anwendung tippen, die von links nach rechts läuft. Symbolleiste, Menüs, Dropdowns, Dialoge und Statusleiste werden alle gespiegelt:

editor.setRtlUserInterface(true);
editor.toggleRtlUserInterface();
editor.isRtlUserInterface();

// config: rtlUserInterface — "auto" (follow the document), true, or false

Die Richtung der Oberfläche bleibt streng von der Richtung des Inhalts getrennt: Das Spiegeln der Oberfläche schreibt nichts in Ihr Dokument, und getHTMLCode() bleibt unverändert. Bei mehreren Editoren auf einer Seite ist nur der betroffen, den Sie spiegeln. Symbole werden absichtlich nicht gespiegelt – auch Word spiegelt seine Symbolleistensymbole nicht, und die Symbole für fett, kursiv und Ausrichtung bedeuten in beiden Richtungen dasselbe. Nur die Symbole, die eine Leserichtung ausdrücken (Einzug vergrößern, verkleinern, Menüpfeile), werden gespiegelt.

Fairerweise: Sowohl CKEditor als auch TinyMCE liefern eine grundlegende Textrichtung im kostenlosen Kern mit; die automatische Erkennung, die Bidi-Isolation und das Spiegeln der Oberfläche gehen hier darüber hinaus, aber für die Grundfunktion selbst verlangen sie kein Geld.

Ziehgriff & Blöcke umordnen

Fahren Sie mit der Maus über einen beliebigen Absatz, eine Überschrift, Liste, Tabelle oder einen Hinweiskasten, und daneben erscheint ein Griff ⠿ – ziehen Sie daran, um den ganzen Block zu verschieben, wobei eine Einfügelinie genau zeigt, wo er landen wird. Diese Art, Blöcke umzuordnen, hat Notion zum Standard gemacht, und sie ist ein weit besserer Weg, eine Klausel zu verschieben, als Ausschneiden und Einfügen.

// The drag is a thin layer over a scriptable API:
editor.moveBlockUp();               // block containing the caret
editor.moveBlockDown();
editor.moveBlock(el, beforeEl);     // programmatic move

editor.setDragHandleEnabled(false); // hide the grip
// config: dragHandleEnabled, dragHandleKeyboard

Alt+Shift+↑/↓ verschiebt den Block, in dem der Cursor steht – dieselben Tasten, mit denen Word Absätze verschiebt. Das ist kein Bonus-Kürzel: Eine Funktion zum Umordnen nur per Zeiger ist per Tastatur nicht bedienbar, erst der Weg über die Tastatur macht die Funktion überhaupt barrierefrei. Auf beiden Wegen wandert der Cursor mit dem verschobenen Block mit.

Der Griff und die Einfügelinie liegen vollständig außerhalb des bearbeitbaren Bereichs und können daher schon strukturell nicht in Ihrem gespeicherten HTML erscheinen. Das Ziehen verwendet einfache Mausereignisse statt des nativen Drag & Drop von HTML5, das innerhalb von contenteditable überall nur halb funktioniert – es startet ein Ziehen von Text, zeichnet das falsche Vorschaubild und legt serialisiertes HTML ab, statt den Knoten zu verschieben. In einem RTL-Dokument erscheint der Griff rechts, wo die Zeile beginnt.

PDF-Export mit echtem Text

Es gibt zwei PDF-Exporter, und der Unterschied ist wichtig. html2pdf rendert das Dokument mit html2canvas und legt auf jede Seite ein Bild: pixelgenau und die richtige Wahl, wenn genau das visuelle Layout das Ergebnis sein soll. Aber das Ergebnis ist ein Bild eines Dokuments – nichts ist auswählbar oder durchsuchbar, ein Screenreader findet eine leere Datei, Links sind gemalt statt anklickbar, und eine Seite reiner Text kostet Hunderte Kilobyte.

exportpdf schreibt stattdessen direkt das PDF-Dateiformat. Text bleibt Text. Es verwendet die 14 Standardschriften von PDF, die nicht eingebettet werden müssen – damit entfällt das Subsetting von Schriften, der wirklich schwierige Teil eines PDF-Generators, und eine typische Seite bleibt unter 15 KB. Es wird keine Bibliothek geladen und nichts hochgeladen.

  • Überschriften, fett/kursiv/unterstrichen/durchgestrichen, Farbe, Schriftgröße, Aufzählungen und nummerierte Listen, Tabellen mit wiederholten Kopfzeilen, Blockzitate, Linien und Bilder
  • Links werden zu echten Link-Annotationen im PDF, nicht zu unterstrichenem Text
  • Seitengröße, Ausrichtung und Ränder stammen aus der Seiteneinrichtung des Dokuments selbst, sodass das PDF der Seitenansicht und dem Word-Export entspricht
  • Explizite Seitenumbrüche werden beachtet, und eine Überschrift bleibt nie allein am Fuß einer Seite stehen
  • Titel, Autor und Sprache des Dokuments werden als Metadaten geschrieben

Eine Feinheit, die man kennen sollte: Ein Viewer setzt Text mit den eigenen Metriken der Schrift, sodass ein Generator, der Zeilen mit eigenen Zahlen umbricht, davon abweicht und über den Rand hinausläuft. Hier wird jedes Zeichen gemessen, und diese Breiten werden als /Widths in das PDF geschrieben, die ein Viewer beachten muss – Layout und Darstellung verwenden dann zwangsläufig dieselben Zahlen. Geprüft mit pdf.js (dem Parser, den Firefox mitliefert): Text lässt sich sauber extrahieren, Links werden aufgelöst, und nichts überschreitet den Rand.

Getaggt für Barrierefreiheit

Auswählbarer Text ist notwendig, aber nicht ausreichend. Ein ungetaggtes PDF fällt trotzdem durch ein Audit nach Section 508 oder EN 301 549: Assistive Technologie erhält einen flachen Strom von Wörtern ohne Überschriften, ohne Listenstruktur, ohne Tabellensemantik und ohne garantierte Lesereihenfolge. Der Export schreibt einen vollständigen Strukturbaum, sodass sich im Dokument navigieren lässt, statt es nur lesen zu können.

  • H1–H6 für Überschriften, P für Absätze, BlockQuote für Zitate
  • L / LI / Lbl / LBody, sodass ein Screenreader „Liste, 3 Einträge“ ansagt statt drei loser Absätze
  • Table / TR / TH / TD mit /Scope an Kopfzellen – der Unterschied zwischen „Umsatz, 1,240“ und bloß „1,240“
  • Figure-Elemente tragen /Alt aus dem Alt-Text des Bildes
  • Linien, Zellrahmen, der Zitatbalken und die laufende Kopf- und Fußzeile werden als Artefakte gekennzeichnet und damit aus der Lesereihenfolge genommen – eine auf jeder Seite vorgelesene, wiederholte Kopfzeile gehört zu den häufigsten Audit-Befunden
  • /MarkInfo, /StructTreeRoot, a /ParentTree, /Lang und /DisplayDocTitle, damit der Viewer den Titel des Dokuments statt seines Dateinamens ansagt

Der Baum wird geprüft, indem er über pdf.js so zurückgelesen wird, wie es assistive Technologie täte, nicht durch Untersuchen der Bytes: 25 Zusicherungen zu jeder Rolle, zur Lesereihenfolge des Dokuments und zum Fehlen von Artefakten im Inhaltsbaum.

editor.exportToPdf("quarterly-report");
const bytes = editor.getPdfBytes({ title: "Q3", author: "Finance" });

// config
pdfExportFont: "helvetica" | "times" | "courier"
pdfExportBaseFontSize: 11
pdfExportHeader: "Confidential"
pdfExportFooter: "Page {page} of {total}"

Echter .docx-Export im Browser

Der Import las schon clientseitig ein echtes OOXML-Paket. Der Export nicht: wordexport verpackt das HTML in einen MSO-artigen Container und nennt ihn .doc. Word öffnet das, aber es ist keine .docx – der Hin- und Rückweg funktioniert schlecht, Word meckert beim Speichern, und manche Unternehmensrichtlinien blockieren HTML mit doc-Endung direkt. exportdocx schreibt stattdessen ein echtes OOXML-Paket: ein ZIP, erzeugt mit CompressionStream, dem Spiegelbild des DecompressionStream, den der Importer verwendet. Keine Bibliothek, kein Upload.

Er gibt absichtlich genau das aus, was der Importer zurückliest – dieselben Hyperlink-Beziehungen, Nummerierungs-IDs, Teile für Fußnoten und Kommentare und dieselben Konventionen für Revisionen –, sodass HTML → .docx → HTML ein echter Hin- und Rückweg ist statt zweier Funktionen, die zufällig eine Dateiendung teilen. In beide Richtungen geprüft: Der Editor importiert seine eigene Ausgabe erneut (Überschriften, Zeichenformatierung, Farbe, Link-URLs, verschachtelte Listen, Tabellen, Zitate, Fußnoten, verfolgte Änderungen und Formeln werden alle wiederhergestellt), und das Paket selbst wird Teil für Teil validiert – jeder XML-Teil wohlgeformt, jede r:id auflösbar, jede numId definiert und jeder Teil in [Content_Types].xml deklariert.

Zwei Details, die von Hand erstellte .docx-Dateien stillschweigend kaputt machen, beide hier berücksichtigt: w:sz ist in Halbpunkten angegeben, wer Punkte schreibt, erhält das ganze Dokument in halber Größe; und ein einziges Steuerzeichen irgendwo in einem Teil lässt Word die Datei ganz ablehnen, statt das Zeichen zu überspringen.

await editor.exportToDocx("quarterly-report");
const blob = await editor.getDocxBlob({ title: "Q3", author: "Finance" });

Sanitizer für Inhalte

„Im Editor wird nichts ausgeführt“ ist der falsche Maßstab. Der Maßstab ist: In der Anwendung, die darstellt, was der Editor gespeichert hat, wird nichts ausgeführt – denn so ist jedes Deployment aufgebaut: Ein Benutzer schreibt Inhalt, er wird gespeichert, und der Browser eines anderen Benutzers stellt ihn dar.

Der Kern des Editors entfernt bereits Inline-Ereignishandler und lehnt javascript: in Links und Formular-Actions ab, und keine von 20 Test-Payloads wurde im Editor ausgeführt. Aber drei Konstrukte überlebten bis in die gespeicherte Ausgabe, und eines davon – <iframe srcdoc> – lief, sobald dieses gespeicherte HTML in einem gewöhnlichen Container dargestellt wurde. Der Sanitizer schließt diese Lücke.

  • Allowlist statt Blocklist – eine Blocklist ist das Versprechen, an jedes Element gedacht zu haben, das HTML je bekommen wird. Unbekannte Elemente werden aufgelöst, sodass der Text, den ein Benutzer darin getippt hat, nie verloren geht.
  • Läuft auf dem Weg hinein (Setzen, Einfügen, Ablegen) und auf dem Weg hinaus. Die Ausgabe ist der garantierte Weg: Sie wird gespeichert, sie arbeitet auf einer losgelösten Kopie und kann daher nie den Cursor stören, und Inhalte von vor dem Plugin werden beim nächsten Speichern bereinigt.
  • URLs werden geprüft, nachdem Steuerzeichen entfernt wurden – java&#9;script: wird von Browsern als javascript: geparst, eine Prüfung des rohen Strings lässt sich also mühelos umgehen.
  • style wird pro Deklaration gefiltert, nicht pro Fragment: Würde man javascript: aus url(javascript:f()) löschen, bliebe url(f()), das dann wie eine harmlose relative URL aussieht und genau die Prüfung besteht, die es abfangen soll.
  • Parst so lange erneut, bis das Ergebnis stabil ist, um Mutation XSS zu verhindern – Markup, das bei einem Parsen harmlos ist und gefährlich wird, sobald der Serialisierer es umgeschrieben hat.
  • target="_blank"-Links erhalten rel="noopener".

Einbettungen funktionieren weiterhin: <iframe> bleibt für Quellen mit https erhalten. Ist keine Allowlist für Hosts konfiguriert, erhält es eine Sandbox, die erlaubt, was eine Videoeinbettung braucht, und Navigation des obersten Fensters, Popups, Modals und Formulare verweigert – ein eingeschleuster Rahmen kann das übergeordnete Dokument nicht skripten, könnte sonst aber das oberste Fenster navigieren, ein typischer Phishing-Zug. Das Setzen von sanitizerAllowedIframeHosts ist die stärkere Kontrolle.

Das ist zusätzliche Verteidigung, kein Ersatz für die Validierung auf dem Server. Sie läuft im Browser, und alles, was im Browser läuft, kann ein Client umgehen, der es einfach nicht ausführt. Bereinigen Sie auf dem Server, bevor Sie speichern, und maskieren Sie bei der Ausgabe. Diese Schicht schützt den ehrlichen Weg und verengt den unehrlichen; sie macht nicht vertrauenswürdiges HTML nicht bedingungslos sicher darstellbar.

contentSanitizer: true                       // default
sanitizerAllowedIframeHosts: ["www.youtube.com"]
sanitizerAllowStyleTags: false               // default
sanitizerAllowTags: ["my-element"]
sanitizerAllowAttributes: ["my-attr"]

editor.sanitizeHtml(html)        // clean a string yourself
editor.getSanitizerReport()      // what has been stripped so far
Alle drei sind in der Basislizenz enthalten

CKEditor 5 stellt Fußnoten hinter seine Tarife Essential, Professional und Custom und Inhaltsverzeichnis, Format übertragen, Groß-/Kleinschreibung und Seriendruckfelder hinter „selected CKEditor Plans“. TinyMCE liefert jede dieser Funktionen als Premium-Plugin aus, „only available for paid TinyMCE subscriptions“ – Autokorrektur und Typografie als zwei getrennte. Hier sind sie alle Teil der unbefristeten Lizenz für $129. (Fairerweise: Die automatische Textumwandlung von CKEditor ist in dessen Open-Source-Kern enthalten, bei der Autokorrektur herrscht dort also Gleichstand statt einer Lücke.) Siehe den vollständigen Vergleich.