Anmelden

Erweiterbar von Grund auf

Der Kern von Sumaq bleibt schlank. Wenn ihr mehr braucht, baut ihr es, und das Extension-System macht das unkompliziert.

Was ihr bauen könnt

  • Eigene Formatierungs-Buttons für das Content-Modell eurer Marke
  • CMS-Medienpicker, die eure Asset-Bibliothek öffnen
  • Content-Validierung, die redaktionelle Regeln durchsetzt
  • Markenspezifische Toolbars für verschiedene Inhaltstypen

So geht's.

Eine Extension schreiben

Eine Extension ist ein einfaches Objekt mit einem name, optionalen buttons und Lifecycle-Hooks.

Minimales Beispiel: ein Button, der markierten Text in <span class="highlight"> einwickelt:

function highlightExtension() {
  return {
    name: 'highlight',

    buttons: {
      highlight: {
        label: 'HL',
        title: 'Text hervorheben'
      }
    },

    protectedClasses: ['highlight'],

    init(editor) {
      // editor.wrapper, editor.content, editor.shadowRoot sind verfügbar
      // Modale oder Panels hier erstellen, wenn nötig
    },

    onCommand(editor, buttonName) {
      const existing = editor._getClosestElement('SPAN');
      if (existing?.classList.contains('highlight')) {
        // Entfernen: Span auspacken
        const parent = existing.parentNode;
        while (existing.firstChild) parent.insertBefore(existing.firstChild, existing);
        parent.removeChild(existing);
        parent.normalize();
      } else {
        // Anwenden: Selektion in span.highlight einwickeln
        const sel = editor._getSelection();
        if (!sel.toString()) return;
        const range = sel.getRangeAt(0);
        const span = document.createElement('span');
        span.className = 'highlight';
        range.surroundContents(span);
      }
      editor._debounceChange();
    },

    onSelectionChange(editor, context) {
      // Prüfen ob Cursor in einem span.highlight ist
      let node = context.node;
      let current = node?.nodeType === Node.TEXT_NODE ? node.parentNode : node;
      let active = false;
      while (current && current !== editor.content) {
        if (current.tagName === 'SPAN' && current.classList.contains('highlight')) {
          active = true;
          break;
        }
        current = current.parentNode;
      }
      return { highlight: active };
    },

    destroy() {
      // Event-Listener, Modale etc. aufräumen
    }
  };
}

Registrierung:

const editor = new Sumaq('#editor', {
  toolbar: ['bold', 'italic', '|', 'highlight'],
  extensions: [highlightExtension()]
});

Extension-Vertrag

Eigenschaft Typ Pflicht Beschreibung
name string ja Eindeutiger Bezeichner. Wird intern für Command-Routing verwendet.
buttons object nein Map von Button-Namen zu { label, title, icon? }. Jeder Schlüssel muss in der toolbar-Konfiguration stehen, um gerendert zu werden.
protectedClasses string[] nein CSS-Klassen, die diese Extension verwaltet. Überleben Bereinigung und getHTML()-Ausgabe.
init(editor) function nein Wird einmal aufgerufen, nachdem das Editor-DOM erstellt wurde. Für Modale, Element-Caching nutzen.
onCommand(editor, buttonName) function nein Wird aufgerufen, wenn ein Extension-Button geklickt wird. buttonName entspricht dem Schlüssel in buttons.
onSelectionChange(editor, context) function nein Wird bei jeder Selektionsänderung aufgerufen. { [buttonName]: boolean } zurückgeben, um aria-pressed zu aktualisieren.
destroy() function nein Wird aufgerufen, wenn der Editor zerstört wird. Listener und DOM aufräumen.

Context-Objekt (übergeben an onSelectionChange)

Eigenschaft Typ Beschreibung
nodeNodeDer Ankerknoten der Selektion
blockTagstring|nullAktuelles Block-Element-Tag ('p', 'h2', 'li', etc.)
blockElementElement|nullDas tatsächliche Block-DOM-Element
inlineTagsSetAktive Inline-Formatierung ('bold', 'italic', etc.)
inLinkbooleanCursor ist innerhalb eines <a href>
inAnchorbooleanCursor ist innerhalb eines <a id> (Anker ohne href)
inTablebooleanCursor ist innerhalb einer <table>
inBlockquotebooleanCursor ist innerhalb eines <blockquote>
textAlignstring'left', 'center' oder 'right'

Verfügbare Editor-Eigenschaften

Innerhalb von init, onCommand und onSelectionChange stellt der editor-Parameter bereit:

Eigenschaft Verwendung
editor.contentDas contenteditable-Element
editor.wrapperDer Editor-Wrapper (Modale hier anhängen)
editor.shadowRootDer Shadow-Root
editor._instanceIdEindeutige ID: verwenden für Element-IDs (z.B. sumaq-${editor._instanceId}-yourext-close)
editor._getSelection()Gibt die aktuelle Selektion zurück. Nutzt shadowRoot.getSelection() wo verfügbar (Chrome/Edge) für korrekte Knoten im Shadow DOM, fällt auf window.getSelection() zurück.
editor._getClosestElement(tagName)Sucht vom Cursor nach oben zum nächsten Vorfahren mit passendem tagName. Gibt das Element oder null zurück.
editor._saveSelection()Speichert den aktuellen Selektionsbereich. Vor dem Öffnen eines Dialogs aufrufen. Der Dialog übernimmt den Fokus und die Selektion geht sonst verloren.
editor._restoreSelection()Stellt die gespeicherte Selektion wieder her und fokussiert den Inhaltsbereich. Nach dem Schließen eines Dialogs aufrufen, bevor Inhalte eingefügt werden.
editor._debounceChange()Löst das entprellte 'change'-Event aus und ruft config.onChange auf, falls gesetzt. Nach jeder DOM-Modifikation in der Extension aufrufen.

Schließen-Button

Wenn eure Extension einen Dialog verwendet, importiert closePath aus icons.js für das Schließen-Button-SVG:

import { closePath } from '../icons.js';

Dann in eurem Dialog-HTML:

<button type="button" class="sumaq-dialog__close" aria-labelledby="${closeId}">
  <svg class="icon" role="img" viewBox="0 0 448 512">
    <title id="${closeId}">Schließen</title>
    <path d="${closePath}"/>
  </svg>
</button>

Verwendet sumaq-${editor._instanceId}-yourext-close als closeId, damit IDs über Instanzen hinweg eindeutig bleiben.

Regeln

  1. Button-Namen müssen eindeutig sein über Kern und alle Extensions hinweg
  2. Toolbar-Konfiguration steuert Sichtbarkeit: ein Button in buttons, der nicht in toolbar steht, wird nicht gerendert
  3. protectedClasses ist die Whitelist: jede nicht gelistete Klasse wird durch Bereinigung entfernt. Der Kern-Editor registriert text-center und text-right vor. Diese Namen nicht wiederverwenden.
  4. Extensions besitzen ihr DOM: Modale in init erstellen, in destroy aufräumen
  5. editor.content.innerHTML nicht direkt modifizieren: DOM-Methoden verwenden und editor._debounceChange() aufrufen. Der Editor betreibt einen Mutation-Observer, der Inhalte bei jeder Änderung normalisiert (semantische Tag-Konvertierung, Inline-Style-Bereinigung).