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.
- 1. Das HTML-Gerüst entwerfen
- 2. WYSIWYG Editor stylen
- 3. Funktionen in JavaScript programmieren
- 3.1 Variablen deklarieren
- 3.2 Toolbar Buttons mit Funktionen belegen
- 3.3 Link Modal (Pop-Up) Funktionalität programmieren
- 3.4 Toolbar Buttons aktivieren, wenn Formatierung gewählt wurde
- 3.5 Formatierungen entfernen beim Text einfügen (Paste-Event)
- 3.6 p-Tag als Zeilenumbruch einfügen
- 3.7 Kompletter JavaScript Code
- 4. Fazit
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>
1.3 Modal (Pop-Up) für Links einfügen
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'); } }
3.3 Link Modal (Pop-Up) Funktionalität programmieren
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!
Wie fandest du diesen Beitrag?
Danke für das Einstiegstutorial. Hier noch was zum testen der Funktionenhttps://codepen.io/chrisdavidmills/full/gzYjag/
Danke für den Link! 🙂
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
Thanks! I will observe it when I review this tutorial!
Etwas habe ich noch festgestellt … es gibt eine Fehlermeldung:
Uncaught TypeError: Cannot read property ‚getElementsByClassName‘ of undefined
selbiges problem, Browser: Chrome Version 98.0.4758.102 (Offizieller Build) (64-Bit)
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
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 …
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 nochdocument.execCommand(action, false);
. Diese zweite Zeile muss raus, die haben wir ja in derexecDefaultAction()
-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
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
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
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
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
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
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 🙂
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
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 DuinsertImage
verwenden.LG LH
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.
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
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
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
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.
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