Eigenen WYSIWYG Editor programmieren – mit HTML, CSS & JavaScript

Dich nerven fehlende oder unnötige Funktionen in WYSIWYG Editoren? Kein Problem! Hier zeige ich Dir, wie Du Dir Deinen eigenen voll-funktionsfähigen WYSIWYG Editor mit HTML, CSS und JavaScript programmieren kannst.

WYSIWYG steht für „What you see is what you get“ und bedeutet zu Deutsch etwa „Was Du siehst, ist was du bekommst“. Damit sind Texteditoren gemeint, welche Direkt einen Text mit allen Formatierungen anzeigen und wir die Formatierungen beliebig ändern können. Sie werden auch häufig als Rich Text Editor bezeichnet.

Viele der verfügbaren Editoren, wie TinyMCE funktionieren wirklich gut und sind für die meisten Projekte super einsetzbar. Allerdings findest Du den ein oder anderen Editor vielleicht etwas überladen, zu kompliziert oder Dich reizt es einfach, Deinen eigenen WYSIWYG Editor zu programmieren.

Die folgende Demo ist mit reinem HTML, CSS und purem JavaScript erstellt. In den nächsten Schritten gehe ich auf die Umsetzung dieses WYSIWYG Editors genau ein und am Ende wirst Du in der Lage sein, Deinen eigenen Editor zu programmieren

Hier ist die laufende Demoversion des Editors, den wir jetzt zusammen coden wollen.

Auch auf dieser Seite verwende ich diesen Rich Text Editor für die Kommentare! 🙂

1. Das HTML-Gerüst entwerfen

Unsere Hauptaufgabe im HTML ist es, die Editor Toolbar zu erstellen. Dazu haben wir einen äußeren Container .wp-webdeasy-comment-editor. Dieser schließt einen Container für die Toolbar .toolbar und einen Container für die verschiedenen Ansichten (Visuelle Darstellung & HTML Ansicht) .content-area ein.

<div class="wp-webdeasy-comment-editor">
  <div class="toolbar">
  </div>
  <div class="content-area">
  </div>
</div>

1.1 Die Toolbar

Die Toolbar habe ich in zwei Zeilen angeordnet (.line), es können aber auch beliebig mehr werden. Außerdem gibt es in jeder Zeile mehrere Boxen .box für eine grobe Gliederung der Formatierungsmöglichkeiten.

In einer solchen Box befindet sich immer ein Span-Element mit einer Data Action (data-action). Diese Data Action beinhaltet den Befehl, der später auf dem markierten Text ausgeführt werden soll. Außerdem haben einige Elemente noch einen Data Tagname (data-tag-name). Dieser ist später wichtig, damit wir den Button aktiv setzen können, wenn die aktuelle Textauswahl eine bestimmte Formatierung hat.

So sehen die beiden Toolbar Zeilen im HTML aus:

<div class="line">

  <div class="box">
    <span class="editor-btn icon smaller" data-action="bold" data-tag-name="b" title="Bold">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/bold.png"/>
    </span>
    <span class="editor-btn icon smaller" data-action="italic" data-tag-name="i" title="Italic">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/italic.png"/>
    </span>
    <span class="editor-btn icon smaller" data-action="underline" data-tag-name="u" title="Underline">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/underline.png"/>
    </span>
    <span class="editor-btn icon smaller" data-action="strikeThrough" data-tag-name="strike" title="Strike through">
      <img src="https://img.icons8.com/fluency-systems-filled/30/000000/strikethrough.png"/>
    </span>
  </div>

  <div class="box">
    <span class="editor-btn icon has-submenu">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/align-left.png"/>
      <div class="submenu">
        <span class="editor-btn icon" data-action="justifyLeft" data-style="textAlign:left" title="Justify left">
          <img src="https://img.icons8.com/fluency-systems-filled/48/000000/align-left.png"/>
        </span>
        <span class="editor-btn icon" data-action="justifyCenter" data-style="textAlign:center" title="Justify center">
          <img src="https://img.icons8.com/fluency-systems-filled/48/000000/align-center.png"/>
        </span>
        <span class="editor-btn icon" data-action="justifyRight" data-style="textAlign:right" title="Justify right">
          <img src="https://img.icons8.com/fluency-systems-filled/48/000000/align-right.png"/>
        </span>
        <span class="editor-btn icon" data-action="formatBlock" data-style="textAlign:justify" title="Justify block">
          <img src="https://img.icons8.com/fluency-systems-filled/48/000000/align-justify.png"/>
        </span>
      </div>
    </span>
    <span class="editor-btn icon" data-action="insertOrderedList" data-tag-name="ol" title="Insert ordered list">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/numbered-list.png"/>
    </span>
    <span class="editor-btn icon" data-action="insertUnorderedList" data-tag-name="ul" title="Insert unordered list">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/bulleted-list.png"/>
    </span>
    <span class="editor-btn icon" data-action="outdent" title="Outdent" data-required-tag="li">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/outdent.png"/>
    </span>
    <span class="editor-btn icon" data-action="indent" title="Indent">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/indent.png"/>
    </span>

  </div>
  <div class="box">
    <span class="editor-btn icon" data-action="insertHorizontalRule" title="Insert horizontal rule">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/horizontal-line.png"/>
    </span>
  </div>

</div>
<div class="line">

  <div class="box">
    <span class="editor-btn icon smaller" data-action="undo" title="Undo">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/undo--v1.png"/>
    </span>
    <span class="editor-btn icon" data-action="removeFormat" title="Remove format">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/remove-format.png"/>
    </span>
  </div>

  <div class="box">
    <span class="editor-btn icon smaller" data-action="createLink" title="Insert Link">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/add-link.png"/>
    </span>
    <span class="editor-btn icon smaller" data-action="unlink" data-tag-name="a" title="Unlink">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/delete-link.png"/>
    </span>
  </div>

  <div class="box">
    <span class="editor-btn icon" data-action="toggle-view" title="Show HTML-Code">
      <img src="https://img.icons8.com/fluency-systems-filled/48/000000/source-code.png"/>
    </span>
  </div>

</div>

Die Data Action ist der Befehl, der später auf dem markierten Text ausgeführt wird. Dazu gibt es eine Liste der MDN web docs. Du kannst den Editor also ganz einfach um weitere Befehle hier erweitern.

1.2 Visuelle- und HTML-Ansicht

Im Inhaltsbereich haben wir zwei Bereiche: Eine HTML Ansicht und eine visuelle Ansicht. Dazu legen wir einen Container .visuell-view an, welcher zusätzlich die Eigenschaft contenteditable bekommt. Diese Eigenschaft sorgt dafür, dass wir Inhalt direkt Inline ohne Input bearbeiten können. Probier‘ das gern mal aus, falls Du diese Funktion nicht kennst.

<div class="visuell-view" contenteditable>
</div>

Außerdem fügen wir ein Textarea .html-view für die HTML-Ansicht ein, da wir später im Editor zwischen HTML- und visueller Ansicht wechseln möchten.

<textarea class="html-view"></textarea>

Dieses Modal wird geöffnet, wenn wir einen Link einfügen wollen. Dort gibt es die Möglichkeit den Link einzutragen und zu wählen, ob man den Link in einem neuen Fenster öffnen möchte.

<div class="modal">
  <div class="modal-bg"></div>
  <div class="modal-wrapper">
    <div class="close">✖</div>
    <div class="modal-content" id="modalCreateLink">
      <h3>Insert Link</h3>
      <input type="text" id="linkValue" placeholder="Link (example: https://webdeasy.de/)">
      <div class="row">
        <input type="checkbox" id="new-tab">
        <label for="new-tab">Open in new Tab?</label>
      </div>
      <button class="done">Done</button>
    </div>
  </div>
</div>

1.4 Kompletter HTML Code

Und so sieht der gesamte HTML-Code im Überblick aus:

<div class="wp-webdeasy-comment-editor">
  <div class="toolbar">
    <div class="line">
      <div class="box">
        <span
          class="editor-btn icon smaller"
          data-action="bold"
          data-tag-name="b"
          title="Bold"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/bold.png"
          />
        </span>
        <span
          class="editor-btn icon smaller"
          data-action="italic"
          data-tag-name="i"
          title="Italic"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/italic.png"
          />
        </span>
        <span
          class="editor-btn icon smaller"
          data-action="underline"
          data-tag-name="u"
          title="Underline"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/underline.png"
          />
        </span>
        <span
          class="editor-btn icon smaller"
          data-action="strikeThrough"
          data-tag-name="strike"
          title="Strike through"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/30/000000/strikethrough.png"
          />
        </span>
      </div>

      <div class="box">
        <span class="editor-btn icon has-submenu">
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/align-left.png"
          />
          <div class="submenu">
            <span
              class="editor-btn icon"
              data-action="justifyLeft"
              data-style="textAlign:left"
              title="Justify left"
            >
              <img
                src="https://img.icons8.com/fluency-systems-filled/48/000000/align-left.png"
              />
            </span>
            <span
              class="editor-btn icon"
              data-action="justifyCenter"
              data-style="textAlign:center"
              title="Justify center"
            >
              <img
                src="https://img.icons8.com/fluency-systems-filled/48/000000/align-center.png"
              />
            </span>
            <span
              class="editor-btn icon"
              data-action="justifyRight"
              data-style="textAlign:right"
              title="Justify right"
            >
              <img
                src="https://img.icons8.com/fluency-systems-filled/48/000000/align-right.png"
              />
            </span>
            <span
              class="editor-btn icon"
              data-action="formatBlock"
              data-style="textAlign:justify"
              title="Justify block"
            >
              <img
                src="https://img.icons8.com/fluency-systems-filled/48/000000/align-justify.png"
              />
            </span>
          </div>
        </span>
        <span
          class="editor-btn icon"
          data-action="insertOrderedList"
          data-tag-name="ol"
          title="Insert ordered list"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/numbered-list.png"
          />
        </span>
        <span
          class="editor-btn icon"
          data-action="insertUnorderedList"
          data-tag-name="ul"
          title="Insert unordered list"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/bulleted-list.png"
          />
        </span>
        <span
          class="editor-btn icon"
          data-action="outdent"
          title="Outdent"
          data-required-tag="li"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/outdent.png"
          />
        </span>
        <span class="editor-btn icon" data-action="indent" title="Indent">
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/indent.png"
          />
        </span>
      </div>
      <div class="box">
        <span
          class="editor-btn icon"
          data-action="insertHorizontalRule"
          title="Insert horizontal rule"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/horizontal-line.png"
          />
        </span>
      </div>
    </div>
    <div class="line">
      <div class="box">
        <span class="editor-btn icon smaller" data-action="undo" title="Undo">
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/undo--v1.png"
          />
        </span>
        <span
          class="editor-btn icon"
          data-action="removeFormat"
          title="Remove format"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/remove-format.png"
          />
        </span>
      </div>

      <div class="box">
        <span
          class="editor-btn icon smaller"
          data-action="createLink"
          title="Insert Link"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/add-link.png"
          />
        </span>
        <span
          class="editor-btn icon smaller"
          data-action="unlink"
          data-tag-name="a"
          title="Unlink"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/delete-link.png"
          />
        </span>
      </div>

      <div class="box">
        <span
          class="editor-btn icon"
          data-action="toggle-view"
          title="Show HTML-Code"
        >
          <img
            src="https://img.icons8.com/fluency-systems-filled/48/000000/source-code.png"
          />
        </span>
      </div>
    </div>
  </div>
  <div class="content-area">
    <div class="visuell-view" contenteditable></div>
    <textarea class="html-view"></textarea>
  </div>
</div>

<div class="modal">
  <div class="modal-bg"></div>
  <div class="modal-wrapper">
    <div class="close">✖</div>
    <div class="modal-content" id="modalCreateLink">
      <h3>Insert Link</h3>
      <input
        type="text"
        id="linkValue"
        placeholder="Link (example: https://webdeasy.de/)"
      />
      <div class="row">
        <input type="checkbox" id="new-tab" />
        <label for="new-tab">Open in new Tab?</label>
      </div>
      <button class="done">Done</button>
    </div>
  </div>
</div>

In meinem Editor verwende ich Icons von Icons8. Deshalb muss ich einen entsprechenden Hinweis auf meiner Seite einfügen. Falls Du eigene Icons verwendest entfällt das für Dich natürlich.

<a href="https://icons8.com/">Icons by Icons8</a>

2. WYSIWYG Editor stylen

Ich habe hier meinen SCSS Code in normales CSS umgewandelt, damit ihn jeder verstehen kann.

Hierzu erkläre ich aber nichts weiter, da CSS Grundlagen klar sein sollten, wenn man einen solchen Editor programmieren möchte. Natürlich kannst Du auch hier Deine eigenen Styles verwenden.

body {
  margin: 0;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  font-family: 'Helvetica Neue', 'Helvetica', arial, sans-serif;
}
/* WYSIWYG Editor */
.wp-webdeasy-comment-editor {
  width: 40rem;
  min-height: 18rem;
  box-shadow: 0 0 4px 1px rgba(0, 0, 0, 0.3);
  border-top: 6px solid #4a4a4a;
  border-radius: 3px;
  margin: 2rem 0;
}
.wp-webdeasy-comment-editor .toolbar {
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
.wp-webdeasy-comment-editor .toolbar .line {
  display: flex;
  border-bottom: 1px solid #e2e2e2;
}
.wp-webdeasy-comment-editor .toolbar .line:last-child {
  border-bottom: none;
}
.wp-webdeasy-comment-editor .toolbar .line .box {
  display: flex;
  border-left: 1px solid #e2e2e2;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn {
  display: block;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  transition: 0.2s ease all;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn:hover, .wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.active {
  background-color: #e1e1e1;
  cursor: pointer;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.icon img {
  width: 15px;
  padding: 9px;
  box-sizing: content-box;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.icon.smaller img {
  width: 16px;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu {
  width: 20px;
  padding: 0 10px;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu::after {
  content: '';
  width: 6px;
  height: 6px;
  position: absolute;
  background-image: url(https://img.icons8.com/ios-glyphs/30/000000/chevron-down.png);
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
  right: 4px;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu .submenu {
  display: none;
  position: absolute;
  top: 34px;
  left: -1px;
  z-index: 10;
  background-color: #FFF;
  border: 1px solid #b5b5b5;
  border-top: none;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu .submenu .btn {
  width: 39px;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu .submenu:hover {
  display: block;
}
.wp-webdeasy-comment-editor .toolbar .line .box .editor-btn.has-submenu:hover .submenu {
  display: block;
}
.wp-webdeasy-comment-editor .content-area {
  padding: 15px 12px;
  line-height: 1.5;
}
.wp-webdeasy-comment-editor .content-area .visuell-view {
  outline: none;
  min-height: 12rem;
}
.wp-webdeasy-comment-editor .content-area .visuell-view p {
  margin: 12px 0;
}
.wp-webdeasy-comment-editor .content-area .html-view {
  outline: none;
  display: none;
  width: 100%;
  height: 200px;
  border: none;
  resize: none;
}
/* Modal */
.modal {
  z-index: 40;
  display: none;
}
.modal .modal-wrapper {
  background-color: #FFF;
  padding: 1rem;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 20rem;
  min-height: 10rem;
  z-index: 41;
}
.modal .modal-wrapper .close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  cursor: pointer;
}
.modal .modal-wrapper .modal-content {
  flex-direction: column;
}
.modal .modal-wrapper .modal-content h3 {
  margin-top: 0;
}
.modal .modal-wrapper .modal-content input {
  margin: 1rem 0;
  padding: 0.5rem;
}
.modal .modal-wrapper .modal-content input[type="text"] {
  width: calc(100% - 1rem);
}
.modal .modal-wrapper .modal-content .row label {
  margin-left: 0.5rem;
}
.modal .modal-wrapper .modal-content button {
  background-color: #D2434F;
  border: 0;
  color: #FFF;
  padding: 0.5rem 1.2rem;
  cursor: pointer;
}
.modal .modal-bg {
  position: fixed;
  background-color: rgba(0, 0, 0, 0.3);
  width: 100vw;
  height: 100vh;
  top: 0;
  left: 0;
}

3. Funktionen in JavaScript programmieren

3.1 Variablen deklarieren

Im JavaScript müssen wir nun noch einige Funktionen umsetzen. Dazu deklarieren und initialisieren wir zuerst wichtige Elemente unseres Editors:

const editor = document.getElementsByClassName('wp-webdeasy-comment-editor')[0];
const toolbar = editor.getElementsByClassName('toolbar')[0];
const buttons = toolbar.querySelectorAll('.editor-btn:not(.has-submenu)');
const contentArea = editor.getElementsByClassName('content-area')[0];
const visuellView = contentArea.getElementsByClassName('visuell-view')[0];
const htmlView = contentArea.getElementsByClassName('html-view')[0];
const modal = document.getElementsByClassName('modal')[0];

3.2 Toolbar Buttons mit Funktionen belegen

Um nicht jede Funktion einzeln zu programmieren haben wir im HTML bereits eine Data Action (data-action) mit dem Befehl angelegt. Nun registrieren wir einfach den Klick auf diese Buttons in einer Schleife:

for(let i = 0; i < buttons.length; i++) {
  let button = buttons[i];
  
  button.addEventListener('click', function(e) {
  });
}

Mit folgender Zeile lesen wir die Action aus der Data Action (im HTML) aus.

let action = this.dataset.action;

Wir bauen ein switch-case Statement ein, da das Einfügen eines Links und das Umschalten der HTML Ansicht und der visuellen Ansicht noch mehr von uns fordert.

switch(action) {
  case 'toggle-view':
    execCodeAction(this, editor);
    break;
  case 'createLink':
    execLinkAction();
    break;
  default:
    execDefaultAction(action);
}

Für „normale“ Funktionen verwenden wir die execDefaultAction(action) Funktion. Dort wird lediglich die execCommand() Funktion von JavaScript mit der Data Action des jeweiligen Buttons ausgeführt.

function execDefaultAction(action) {
  document.execCommand(action, false);
}

JavaScript stellt uns eine tolle Funktion document.execCommand() bereit. Diese ermöglicht es uns, unsere Action auf den markierten Text anzuwenden. Die Dokumentation zu dieser Funktion findest Du hier.

Die Funktion execCommand() ist zwar deprecated, wird allerdings aktuell noch von allen Browsern unterstützt. Sobald es hier zu Problemen kommen sollte, werde ich diesen Beitrag updaten!

Der zweite Parameter von execCommand() muss auf false stehen. Damit deaktivieren wir eine kleine UI, die z.B. in alten Internet Explorer Versionen angezeigt werden würde. Das brauchen wir aber nicht und Firefox oder Google Chrome unterstützen diese Funktionen sowieso nicht.

Wenn wir zwischen der HTML Ansicht und der visuellen Ansicht umschalten wollen, blenden wir jeweils die andere ein und tauschen die Inhalte.

function execCodeAction(button, editor) {

  if(button.classList.contains('active')) { // show visuell view
    visuellView.innerHTML = htmlView.value;
    htmlView.style.display = 'none';
    visuellView.style.display = 'block';

    button.classList.remove('active');     
  } else {  // show html view
    htmlView.innerText = visuellView.innerHTML;
    visuellView.style.display = 'none';
    htmlView.style.display = 'block';

    button.classList.add('active'); 
  }
}

Als nächstes wollen wir noch einen Link einfügen können. Dazu habe ich im HTML bereits ein Modal, also eine Art Pop-Up vorgesehen.

In der folgenden Funktion wird dieser eingeblendet und die aktuelle Textauswahl des Editors über saveSelection() gespeichert. Das ist notwendig, da wir in unserem Popup ein anderes Element fokusieren und dadurch unsere Textauswahl im Editor verschwindet. Danach werden noch der Schließen- und Submit-Button angelegt.

function execLinkAction() {  
  modal.style.display = 'block';
  let selection = saveSelection();

  let submit = modal.querySelectorAll('button.done')[0];
  let close = modal.querySelectorAll('.close')[0];
}
function saveSelection() {
    if(window.getSelection) {
        sel = window.getSelection();
        if(sel.getRangeAt && sel.rangeCount) {
            let ranges = [];
            for(var i = 0, len = sel.rangeCount; i < len; ++i) {
                ranges.push(sel.getRangeAt(i));
            }
            return ranges;
        }
    } else if (document.selection && document.selection.createRange) {
        return document.selection.createRange();
    }
    return null;
}

Nun benötigen wir noch ein click Event zum Einfügen des Links. Dort speichern wir uns zusätzlich ab, ob der Link in einem neuen Fenster geöffnet werden soll, laden die Selection aus dem Texteditor wieder mit restoreSelection() und legen dann dafür in Zeile 13 ein neues a Element an und setzen den Link aus dem Link-Input.

In Zeile 16 fügen wir dann den erstellten Link um die Textauswahl herum ein.

Das Modal wird dann noch geschlossen, der Link-Input gesäubert und alle Events sauber deregistriert.

function execLinkAction() {  
  // ...  
  // done button active => add link
  submit.addEventListener('click', function(e) {
    e.preventDefault();
    let newTabCheckbox = modal.querySelectorAll('#new-tab')[0];
    let linkInput = modal.querySelectorAll('#linkValue')[0];
    let linkValue = linkInput.value;
    let newTab = newTabCheckbox.checked;    
    
    restoreSelection(selection);
    
    if(window.getSelection().toString()) {
      let a = document.createElement('a');
      a.href = linkValue;
      if(newTab) a.target = '_blank';
      window.getSelection().getRangeAt(0).surroundContents(a);
    }

    modal.style.display = 'none';
    linkInput.value = '';
    
    // deregister modal events
    submit.removeEventListener('click', arguments.callee);
    close.removeEventListener('click', arguments.callee);
  });  
  // ...
}
function restoreSelection(savedSel) {
    if(savedSel) {
        if(window.getSelection) {
            sel = window.getSelection();
            sel.removeAllRanges();
            for(var i = 0, len = savedSel.length; i < len; ++i) {
                sel.addRange(savedSel[i]);
            }
        } else if(document.selection && savedSel.select) {
            savedSel.select();
        }
    }
}

Dem Schließen-Button geben wir auch noch eine Funktion, bei dem einfach das Modal ausgeblendet wird, der Link-Input geleert und die beiden Events deregistriert werden.

function execLinkAction() {  
  // ...  
  close.addEventListener('click', function(e) {
    e.preventDefault();
    let linkInput = modal.querySelectorAll('#linkValue')[0];
    
    modal.style.display = 'none';
    linkInput.value = '';
    
    // deregister modal events
    submit.removeEventListener('click', arguments.callee);
    close.removeEventListener('click', arguments.callee);
  });
}

3.4 Toolbar Buttons aktivieren, wenn Formatierung gewählt wurde

Ist im WYSIWYG Editor ein Text ausgewählt, wollen wir auch den entsprechenden Formatierungsbutton hervorheben. Damit wissen wir immer, welche Formatierung ein Wort oder Absatz hat.

Dazu fügen wir ganz oben, direkt nach der Deklaration der Variablen die Registrierung des selectionchange Events ein.

// add active tag event
document.addEventListener('selectionchange', selectionChange);

Die Callback-Funktion legen wir dann an, die erstmal von allen Toolbar Buttons mit .active Klasse, diese Klasse entfernt. Danach überprüfen wir noch, ob unsere Auswahl überhaupt in unserem WYSIWYG Editor liegt (Zeile 12). Anschließend rufen wir die parentTagActive() Funktion auf und übergeben den ersten Parent HTML-Tag der aktuellen Textauswahl.

function selectionChange(e) {
  
  for(let i = 0; i < buttons.length; i++) {
    let button = buttons[i];
    
    // don't remove active class on code toggle button
    if(button.dataset.action === 'toggle-view') continue;
    
    button.classList.remove('active');
  }
  
  if(!childOf(window.getSelection().anchorNode.parentNode, editor)) return false;
  
  parentTagActive(window.getSelection().anchorNode.parentNode);
}

Und hier noch die in Zeile 12 verwendete childOf() Funktion:

function childOf(child, parent) {
  return parent.contains(child);
}

Die parentTagActive() Funktion habe ich rekursiv definiert, so dass es auch mehrere aktive Tags geben kann. Ist also ein Wort kursiv, fett und unterstrichen werden auch alle drei Toolbar Buttons aktiv gesetzt. Aus diesem Grund haben die einzelnen Buttons im HTML den Data Tagname (data-tag-name) bekommen.

Die Textausrichtung wird nach dem gleichen Prinzip behandelt, so dass wir angezeigt bekommen, ob der Text linksbündig, rechtsbündig, im Blocksatz oder zentriert angeordnet ist.

function parentTagActive(elem) {
  if(!elem ||!elem.classList || elem.classList.contains('visuell-view')) return false;
  
  let toolbarButton;
  
  // active by tag names
  let tagName = elem.tagName.toLowerCase();
  toolbarButton = document.querySelectorAll(`.toolbar .editor-btn[data-tag-name="${tagName}"]`)[0];
  if(toolbarButton) {
    toolbarButton.classList.add('active');
  }
  
  // active by text-align
  let textAlign = elem.style.textAlign;
  toolbarButton = document.querySelectorAll(`.toolbar .editor-btn[data-style="textAlign:${textAlign}"]`)[0];
  if(toolbarButton) {
    toolbarButton.classList.add('active');
  }
  
  return parentTagActive(elem.parentNode);
}

3.5 Formatierungen entfernen beim Text einfügen (Paste-Event)

Wenn ein Nutzer etwas in den Texteditor einfügt sollen alle Formatierungen von diesem Text entfernt werden, da es ansonsten zu unschönen Formatierungen und komplettem Design-Chaos kommen kann. Dazu registrieren wir das paste Event.

// add paste event
visuellView.addEventListener('paste', pasteEvent);

Es wird dann die pasteEvent() Funktion ausgeführt, welche das normale Einfügen verhindert, sich aus der Zwischenablage des Nutzers den Inhalt als plain Text (also reinen Text) holt und ihn in unseren Editor einfügt.

function pasteEvent(e) {
  e.preventDefault();
  
  let text = (e.originalEvent || e).clipboardData.getData('text/plain');
  document.execCommand('insertHTML', false, text);
}

3.6 p-Tag als Zeilenumbruch einfügen

Eine weitere Verbesserung ist es, wenn automatisch ein <p>-Tag eingefügt wird, sobald der Nutzer Enter drückt. Dazu registrieren wir das keypress Event.

// add paragraph tag on new line
contentArea.addEventListener('keypress', addParagraphTag);

Es wird die addParagraphTag() Funktion aufgerufen. Diese prüft, ob die Enter Taste gedrückt wurde (Keycode 13). Dann wird der aktuelle Block automatisch als <p>-Tag formatiert, wenn es sich bei dem aktuellen Element nicht um ein Listenelement (<li>-Tag) handelt.

function addParagraphTag(evt) {
  if (evt.keyCode == '13') {
    
    // don't add a p tag on list item
    if(window.getSelection().anchorNode.parentNode.tagName === 'LI') return;
    document.execCommand('formatBlock', false, 'p');
  }
}

3.7 Kompletter JavaScript Code

Damit haben wir alle Funktionen hinzugefügt. Und hier noch einmal der vollständige JavaScript Code inklusive Kommentare:

// define vars
const editor = document.getElementsByClassName('wp-webdeasy-comment-editor')[0];
const toolbar = editor.getElementsByClassName('toolbar')[0];
const buttons = toolbar.querySelectorAll('.editor-btn:not(.has-submenu)');
const contentArea = editor.getElementsByClassName('content-area')[0];
const visuellView = contentArea.getElementsByClassName('visuell-view')[0];
const htmlView = contentArea.getElementsByClassName('html-view')[0];
const modal = document.getElementsByClassName('modal')[0];

// add active tag event
document.addEventListener('selectionchange', selectionChange);

// add paste event
visuellView.addEventListener('paste', pasteEvent);

// add paragraph tag on new line
contentArea.addEventListener('keypress', addParagraphTag);

// add toolbar button actions
for(let i = 0; i < buttons.length; i++) {
  let button = buttons[i];
  
  button.addEventListener('click', function(e) {
    let action = this.dataset.action;
    
    switch(action) {
      case 'toggle-view':
        execCodeAction(this, editor);
        break;
      case 'createLink':
        execLinkAction();
        break;
      default:
        execDefaultAction(action);
    }
    
  });
}

/** 
 * This function toggles between visual and html view
 */
function execCodeAction(button, editor) {

  if(button.classList.contains('active')) { // show visuell view
    visuellView.innerHTML = htmlView.value;
    htmlView.style.display = 'none';
    visuellView.style.display = 'block';

    button.classList.remove('active');     
  } else {  // show html view
    htmlView.innerText = visuellView.innerHTML;
    visuellView.style.display = 'none';
    htmlView.style.display = 'block';

    button.classList.add('active'); 
  }
}

/**
 * This function adds a link to the current selection
 */
function execLinkAction() {  
  modal.style.display = 'block';
  let selection = saveSelection();

  let submit = modal.querySelectorAll('button.done')[0];
  let close = modal.querySelectorAll('.close')[0];
  
  // done button active => add link
  submit.addEventListener('click', function(e) {
    e.preventDefault();
    let newTabCheckbox = modal.querySelectorAll('#new-tab')[0];
    let linkInput = modal.querySelectorAll('#linkValue')[0];
    let linkValue = linkInput.value;
    let newTab = newTabCheckbox.checked;    
    
    restoreSelection(selection);
    
    if(window.getSelection().toString()) {
      let a = document.createElement('a');
      a.href = linkValue;
      if(newTab) a.target = '_blank';
      window.getSelection().getRangeAt(0).surroundContents(a);
    }

    modal.style.display = 'none';
    linkInput.value = '';
    
    // deregister modal events
    submit.removeEventListener('click', arguments.callee);
    close.removeEventListener('click', arguments.callee);
  });  
  
  // close modal on X click
  close.addEventListener('click', function(e) {
    e.preventDefault();
    let linkInput = modal.querySelectorAll('#linkValue')[0];
    
    modal.style.display = 'none';
    linkInput.value = '';
    
    // deregister modal events
    submit.removeEventListener('click', arguments.callee);
    close.removeEventListener('click', arguments.callee);
  });
}

/**
 * This function executes all 'normal' actions
 */
function execDefaultAction(action) {
  document.execCommand(action, false);
}

/**
 * Saves the current selection
 */
function saveSelection() {
    if(window.getSelection) {
        sel = window.getSelection();
        if(sel.getRangeAt && sel.rangeCount) {
            let ranges = [];
            for(var i = 0, len = sel.rangeCount; i < len; ++i) {
                ranges.push(sel.getRangeAt(i));
            }
            return ranges;
        }
    } else if (document.selection && document.selection.createRange) {
        return document.selection.createRange();
    }
    return null;
}

/**
 *  Loads a saved selection
 */
function restoreSelection(savedSel) {
    if(savedSel) {
        if(window.getSelection) {
            sel = window.getSelection();
            sel.removeAllRanges();
            for(var i = 0, len = savedSel.length; i < len; ++i) {
                sel.addRange(savedSel[i]);
            }
        } else if(document.selection && savedSel.select) {
            savedSel.select();
        }
    }
}

/**
 * Sets the current selected format buttons active/inactive
 */ 
function selectionChange(e) {
  
  for(let i = 0; i < buttons.length; i++) {
    let button = buttons[i];
    
    // don't remove active class on code toggle button
    if(button.dataset.action === 'toggle-code') continue;
    
    button.classList.remove('active');
  }
  
  if(!childOf(window.getSelection().anchorNode.parentNode, editor)) return false;
  
  parentTagActive(window.getSelection().anchorNode.parentNode);
}

/**
 * Checks if the passed child has the passed parent
 */
function childOf(child, parent) {
  return parent.contains(child);
}

/**
 * Sets the tag active that is responsible for the current element
 */
function parentTagActive(elem) {
  if(!elem ||!elem.classList || elem.classList.contains('visuell-view')) return false;
  
  let toolbarButton;
  
  // active by tag names
  let tagName = elem.tagName.toLowerCase();
  toolbarButton = document.querySelectorAll(`.toolbar .editor-btn[data-tag-name="${tagName}"]`)[0];
  if(toolbarButton) {
    toolbarButton.classList.add('active');
  }
  
  // active by text-align
  let textAlign = elem.style.textAlign;
  toolbarButton = document.querySelectorAll(`.toolbar .editor-btn[data-style="textAlign:${textAlign}"]`)[0];
  if(toolbarButton) {
    toolbarButton.classList.add('active');
  }
  
  return parentTagActive(elem.parentNode);
}

/**
 * Handles the paste event and removes all HTML tags
 */
function pasteEvent(e) {
  e.preventDefault();
  
  let text = (e.originalEvent || e).clipboardData.getData('text/plain');
  document.execCommand('insertHTML', false, text);
}

/**
 * This functions adds a paragraph tag when the enter key is pressed
 */
function addParagraphTag(evt) {
  if (evt.keyCode == '13') {
    
    // don't add a p tag on list item
    if(window.getSelection().anchorNode.parentNode.tagName === 'LI') return;
    document.execCommand('formatBlock', false, 'p');
  }
}

4. Fazit

Wie Du nun siehst, kannst Du relativ einfach einen eigenen WYSIWYG Editor programmieren und nach Deinen Vorstellungen stylen und programmieren. Wenn Dir dieser Beitrag gefallen hat, würde ich mich freuen, wenn Du meinen Blog durch Deinen wiederholten Besuch unterstützt. 🙂

Auf dieser Seite verwende ich auch diesen WYSIWYG-Editor für die WordPress-Kommentare. Schau‘ dir diesen den Link an, um zu sehen, wie einfach das funktioniert!

Ähnliche Beiträge
Beteilige dich an der Unterhaltung

24 Kommentare

  1. Albert Hoffmann sagt:

    Danke für das Einstiegstutorial. Hier noch was zum testen der Funktionenhttps://codepen.io/chrisdavidmills/full/gzYjag/

    1. Lorenz sagt:

      Danke für den Link! 🙂

  2. Uwe Lengler sagt:

    Hi, nice example and simple tool, but :

    the Document.execCommand() is deprecated,

    see : https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand

    Gruß

    Uwe

    1. Lorenz sagt:

      Thanks! I will observe it when I review this tutorial!

  3. Walter Trumpf sagt:

    Etwas habe ich noch festgestellt … es gibt eine Fehlermeldung:
    Uncaught TypeError: Cannot read property ‚getElementsByClassName‘ of undefined

    1. Sven sagt:

      selbiges problem, Browser: Chrome Version 98.0.4758.102 (Offizieller Build) (64-Bit)

      1. Sven sagt:

        Mein problem, habe ich selber lösen können.ich habe den fehler gemacht nur das erste vorkommen .box im css-bereich umbenannt, aber die weiteren vorkommen im css bereich nicht umbenannt, dann entsteht dieser fehler

  4. Walter Trumpf sagt:

    Hallo LH
    Ich finde dein Tool auch grossartig … aber es geht mir wie Jana, die Button zeigen keine Reaktion.
    Im Bereiche habe ich leichte (nicht relevante) Anpassungen vorgenommen, und habe ich 1:1 übernommen.
    Es sind ja sicher „Anfänger“ und JS-Laien die diesen tollen Editor verwenden möchten, deshalb wäre es schon super wenn er ohne Anpassungen funktionieren würde 🙂
    Viele Grüsse und einen guten Start ins 2021, Walter
    nb. Prüf mal, ob die hier abgefragten Klassen wirklich existieren:
    warum können die fehlen? schon da bin ich überfordert – ich habe „untersuchen, dann auf die angegeben Zeile -> Console – es wird nichts angezeigt …

    1. Lorenz sagt:

      Hallo Walter,
      tatsächlich – ich habe den Code gerade noch einmal überprüft: In der Abschnitt wo ich den kompletten JS-Code zusammengefasst habe ist kein Fehler drin. Wenn man aber sich die Teile Stück für Stück zusammenbaut hat sich aber ein kleiner Fehler eingeschlichen. Nach der Zeile let action = this.dataset.action; stand noch document.execCommand(action, false);. Diese zweite Zeile muss raus, die haben wir ja in der execDefaultAction()-Funktion bereits. Danach sollte es funktionieren. 🙂
      Ich habe das Tutorial natürlich entsprechend angepasst.

      Sorry für die Unannehmlichkeiten und einen guten Start in 2021! 🙂
      LG Lorenz

      1. Walter sagt:

        Guten Tag Lorenz
        Vielen Dank für die Beantwortung meiner Frage und für die Anpassungen.
        Nach einigen Versuchen habe ich nun geschafft, dass die „Buttons“ funktionieren – also kann ich den Text nun markieren und zB. fett darstellen. Das Hauptproblem war, dass ich das Script wie gewohnt im (wie auch den ) eingebunden habe, es funktioniert aber nur wenn es nach der steht.
        Nun habe ich noch ein weiteres Problem: mit diesen Script möchte man ja einen Text editieren, das heisst der einen Formularinhalt geben und den dann per „POST“ weiterverarbeiten.
        Ich habe das so wie folgend versucht (aber des Text wird nicht angezeigt wegen „display none“) – wie könnte ich das lösen?
        Vielen Dank für Deine Geduld und viele Grüsse, Walter

        1. Walter sagt:

          Guten Tag Lorenz
          Vielen Dank für die Beantwortung meiner Frage und für die Anpassungen.
          Nach einigen Versuchen habe ich nun geschafft, dass die „Buttons“ funktionieren – also kann ich den Text nun markieren und zB. fett darstellen. Das Hauptproblem war, dass ich das Script wie gewohnt im „head“ (wie auch den „style“) eingebunden habe, es funktioniert aber nur wenn es nach der „textarea“ steht.
          Nun habe ich noch ein weiteres Problem: mit diesen Script möchte man ja einen Text editieren, das heisst der „textarea“ einen Formularinhalt geben und den dann per „POST“ weiterverarbeiten.
          Ich habe das so wie folgend versucht (aber des Text wird nicht angezeigt wegen „display none“) – wie könnte ich das lösen?
          Vielen Dank für Deine Geduld und viele Grüsse, Walter
          form
          div class=“content-area“
          div class=“visuell-view“ contenteditable /div
          textarea class=“html-view“ name=“text“ ?php echo $text ? /textarea
          /div
          input type=’submit‘ value=’submit!‘ name=’send‘ formaction=’edit.php‘ formmethod=’post‘
          /form

          1. Lorenz sagt:

            Hallo Walter,
            ich verstehe das Problem. Statt das textarea auf display: none; zu setzen, kannst Du stattdessen z.B. eine .hide Klasse zu verwenden und im CSS noch folgendes implementieren:
            .hide {
            position: absolute !important;
            top: -9999px !important;
            left: -9999px !important;
            visibility: hidden !important;
            }

            Damit umgehst Du das Problem, hoffe das hilft 🙂

            LG Lorenz

  5. Jana sagt:

    Hallo zusammen,
    ich habe das Script so in eine neue Seite eingefügt… soweit auch super…der Editor wird angezeigt aber es funktioniert nicht…schreiben ja, aber keiner der Button’s funktioniert. ich bin was Java angeht nicht der Hit…
    in dieser zeile wird mir ein zusätzlich ein fehler angezeigt….

    for(let i = 0; i < buttons.length; i++) {
    let button = buttons[i];

    was kann /muss ich machen, damit ich den Editor einsetzten kann!?

    für eure Hilfe bedanke ich mich im Voraus

    Jana

    1. Lorenz sagt:

      Hi Jana,
      was für ein Fehler wird Dir genau angezeigt?
      Ich vermute am ehsten, dass die buttons Variable keinen richtigen Wert hat.
      Prüf mal, ob die hier abgefragten Klassen wirklich existieren:

      const editor = document.getElementsByClassName('editor')[0];
      const toolbar = editor.getElementsByClassName('toolbar')[0];
      const buttons = toolbar.querySelectorAll('.btn:not(.has-submenu)');

      Dazu kannst Du Dir die Variablen einfach mal auf der Konsole ausgeben lassen.

      Liebe Grüße
      LH

    2. Lorenz sagt:

      Hi Jana, @Walter Trump hatte wohl den gleichen Fehler: Ich habe das Tutorial angepasst. Den Fehler kannst Du in meinem Kommentar an Walter vom 31.12.2020 nachlesen.
      LG Lorenz 🙂

  6. Andre sagt:

    Hallo. Super Anleitung. Hat mir super geholfen. Ich bekomme es leider nicht hin, einen Button für Bilder einzufügen. Ich bin leider nicht Fit in Java-Script.
    Gruss Andre

    1. Lorenz sagt:

      Hi, freut mich!
      Das Einfügen von Bildern funktioniert genau wie die Links. Der zweite Parameter muss die Bild-URL sein. Du kannst versuchen, trotz wenig Ahnung von JavaScript das ganze 1:1 von den Links zu übernehmen. Statt createLink musst Du insertImage verwenden.
      LG LH

  7. Harald Meyer sagt:

    Hallo,

    in unserer Firma gibt es Seiten mit „Wiki-Tags“ Wie kann ich die bearbeiten ohne immer komplett in den html-code zu gehen.
    Beispiel: [addresslist|aktivekunden|Kunden]
    Angezeigt wird Kunde, bei Click wir in einem neuen Fenster die aktive Kundenliste angezeigt.
    ich möchte nun per Editor den Befehl und die Beschriftung ändern.

    1. Lorenz sagt:

      Hallo,
      so ohne weitere Hintergrundinformationen kann ich das nicht beantworten. Den Editor einzubauen ist eine Sache, das was bei Ihnen vermutlich relevanter ist, ist die Speicherung der Daten. Dort müssen vermutlich eher Anpassungen gemacht werden, so wie es sich für mich anhört.

      LH

  8. Anna sagt:

    Moin,
    sehr cooles Teil. Vielen Dank.
    Ich bastele damit jetzt schon eine Weile rum und scheitere an zwei dingen:
    1. Die Übergabe des HMTL-Codes aus dem Editor an bekomme ich nicht hin. Hast du da einen Tipp?
    2. Wie bekommt man da eine Link-Funktion rein. Auch das überfordert mich leider.

    Vielen Dank und lieben Gruß
    Anna

    1. Lorenz sagt:

      Hi!
      1. Wohin möchtest Du den Code übergeben?
      2. Für den Link benötigst Du noch eine zusätzliche Eingabe, nämlich den Link den Du einfügen möchtest. Hier ist die Doku von execCommand(), schau hier mal nach dem Punkt „createLink“: https://developer.mozilla.org/de/docs/Web/API/Document/execCommand

      Bei execCommand() musst Du dabei noch den dritten Parameter übergeben. Ich würde also eine extra Abfrage (genau wie bei action == ‚code‘) machen und dann ein Popup oder Ähnliches öffnen, den einzufügenden Link abfragen und zusätzlich an die execCommand()-Funktion übergeben.
      Im Internet findest Du dazu viele Beispiele 🙂

      Viele Grüße
      LH

      1. Anna sagt:

        Moin vielen Dank für die Antwort.
        Beim „an übergeben“ ist mit doch das Ziel abhanden gekommen.
        Ich würde gerne den vom Editor genierten HTML-Code via POST als Formular an eine PHP-Script übergeben.

        1. Lorenz sagt:

          Mit Standard-HTML machst Du einfach ein form-Tag um den ganzen Editor, gibst Dem Textarea ein „name“-Attribut. Das wird dann als GET-Parameter an Dein PHP-Skript geschickt. Hier ein Beispiel: https://wiki.selfhtml.org/wiki/HTML/Formulare/form

          Ich hoffe das hilft Dir weiter! 🙂
          LH

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

bold italic underline strikeThrough
insertOrderedList insertUnorderedList outdent indent
removeFormat
createLink unlink
code

Das könnte dich auch interessieren