JavaScript/Tutorials/Formulare/WYSIWYG-Editor

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Formulare werden häufig benutzt, um dem Benutzer Einträge in Gästebüchern, Foren, Weblogs u. a.m. zu ermöglichen. Um diese Einträge optisch ansprechend gestalten zu können, z. B. durch Hervorheben von Textpassagen oder Anzeigen von Hyperlinks, wird oftmals das Formatieren von einzelnen Eingaben angeboten. Dazu gehört auch, dass dem Anwender eine komfortable Möglichkeit geboten wird, diese Formatierungen im Formular anwenden zu können, ohne detailliertes Wissen über BBCodes, HTML oder Markdown mitzubringen.

Der Kurs zeigt Ihnen, wie Sie anstelle eines nicht formatierbaren textarea-Elements einen kleinen WYSIWYG-Editor programmieren, mit dem Sie Text und ausgewählte Elemente einfügen, sowie markierten Text formatieren können.

Vorüberlegungen

Unser Editor soll sowohl eine WYSIWYG-Ansicht und HTML-Markup anzeigen. Über auch aus Textverarbeitungen bekannte Buttons soll der plain text eines Eingabefeldes zum mit Formatierungen angereicherten rich text editiert werden.

Dabei kann entweder durch Klicken eines Buttons Text in einer bestimmten Formatierung geschrieben, bzw. bereits vorhandener Text nachträglich ausgewählt und dann per Knopfdruck formatiert werden.

HTML-Markup

Im Unterschied zur HTML-Spielwiese soll zwischen der WYSIWYG-Ansicht und dem HTML-Markup umgeschaltet werden können, sodass die content-area zwei weitere Elemente enthält.

HTML-Grundgerüst
<div id="editor">
  <div id="toolbar"></div>
  <div id="content-area">
    <div id="wysiwyg" contenteditable></div>
    <textarea id="html"></textarea>
  </div>
</div>

Für die Toolbar würde sich das menu-Element eignen, das in HTML5.1 angeblich wiederbelebt werden sollte. Auch hier bildet ein div-Element die einfachere Lösung.

Toolbar

Markierter Text soll durch einen Klick auf einen Button in einer bestimmten Weise ausgezeichnet werden. Hier bieten sich button-Elemente geradezu an:

Toolbar mit Buttons
  <div id="toolbar">
    <button data-action="bold">F</button>
    <button data-action="cursive">k</button>
    <button data-action="underline">U</button>    
    <button data-action="quote">Zitat</button>
    <button data-action="send">Senden</button>
    <button data-action="view">Codeansicht</button>    
  </div>

Über die Data-Attribute können mit JavaScript Aktionen ausgelöst und die einzelnen Buttons mit CSS gestylt werden.

JavaScript

Aktivieren der Toolbar

Zunächst programmieren wir einige der Grundfunktionen:

Grundfunktionen ansehen …
document.addEventListener('DOMContentLoaded', function () {

	const editor = document.querySelector('#editor');
	const toolbar = editor.querySelector('#toolbar');	
	toolbar.addEventListener('click', getToolbarAction);	
    
	
	function getToolbarAction(event) {
		let action = event.target.getAttribute('data-action');
		console.log(action);
		// Switching between different views
		if (action == 'view') {
			const wysiwygView = document.querySelector('#wysiwyg');
			const htmlView = document.querySelector('#html');
  			if(wysiwygView.classList.contains('active')) { 
    			htmlView.innerText = wysiwygView.innerHTML;
			} 
			else {  
    			wysiwygView.innerHTML = htmlView.value;
			}
			wysiwygView.classList.toggle('active');
			htmlView.classList.toggle('active');	
		}
	}

});

Das Script wird geladen, sobald DOMContentLoaded ausgelöst hat.

Dann erhält die gesamte Toolbar über addEventListener eine Klickfunktionalität: Bei einen Klick (oder Auswählen mit der Tastatur) wird die Funktion getToolbar aufgerufen.

Sie ermittelt mit event.target, welcher Button geklickt wurde und weist den Wert des data-action-Attributs der Variablen action zu. Zur Kontrolle wird der ensprechende Wert in der Konsole ausgegeben.

Falls der view-Button geklickt wurde, wird mit ClassList.contains überprüft, ob wysiwygView die Klasse active besitzt. In diesem Falle werden mit innerHTML alle Kindelemente ausgelesen und dem value des textarea-Eingabefelds zugewiesen. Mit classList.toggle wird die Klasse active zwischen der WYSIWYG-Ansicht und dem Markup innerhalb des textarea-Felds umgeschaltet.

Die Sackgasse: execCommand

Eigentlich erledigt das contenteditable-Attribut ja schon die eine Hälfte der Arbeit. Darauf aufsetzend bot die document.execCommand()-Methode die Möglichkeit, ausgewählten Text problemlos zu formatieren, bzw. neue Elemente wie Links und Zeilenumbrüche einzufügen.[1]

Da sich die Browser-Hersteller bei der Umsetzung nicht auf einen Standard einigen konnten, ist diese Methode aber bereits wieder obsolet.[2]

 selectedText.execCommand(bold, false);

Diese Methode hätte selektierten Text fett erscheinen lassen, indem es den Text in ein b-Element, im IE in ein strong-Element, gepackt hätte.

Auswahl von Text

Beim Arbeiten im Web spielt »Copy and Paste«, das Kopieren und Einfügen von Text, in vielen Fällen eine wichtige Rolle. Normalerweise nutzt der Anwender den Mauszeiger, um eine Textpassage im HTML-Dokument mit gedrückter Maustaste zu markieren, um diese dann in die Zwischenablage zu übertragen. In einigen Browsern ist aber auch eine Markierung mit der Tastatur möglich.

Solange es JavaScript gibt, wurden Scripte dazu gebraucht, das Auswählen und Kopieren zu vereinfachen. Klassisch ist das ein- oder mehrzeilige Eingabefeld (input- oder textarea-Element), dessen Text beim Fokus über die select-Methode markiert wird:

Beispiel
<input onfocus="this.select()" value="Kopiere mich!">

Diese Technik funktioniert bereits seit JavaScript 1.0. Auf das Eingabefeld konnte man damals nicht verzichten. Im Laufe der Zeit konnte man es mit CSS umformatieren, sodass es nicht mehr als solches erkennbar war – die JavaScript-Funktionalität blieb aber dieselbe.

Für JavaScript ist es nicht nur interessant, eine Markierung zu setzen, sondern auch den vom Leser markierten Text sowie dessen Position im Dokument auszulesen. Dies erreichen Sie mit getSelection().

Auswahl von Text ansehen …
	function getSelectedText() {
		var selectedText = '';
       	if (document.activeElement && document.activeElement.tagName.toLowerCase () == 'textarea' ) {
			var text = document.activeElement.value;
			selectedText = text.substring (document.activeElement.selectionStart, 
			document.activeElement.selectionEnd);
		}
		else { 
			var selRange = window.getSelection ();
			selectedText = selRange.toString ();
		}
        if (selectedText !== '') {
			console.log('selectedText = ' + selectedText);
        }
	}

Die Funktion getSelectedText startet damit, dass sie das gerade fokussierte Element mit Document.activeElement aufruft.

Dann wird im nächsten Zweig geprüft, ob der Browser die Eigenschaft selectionStart unterstützt. Dies ist bei Browsern mit Gecko-Engine (z. B. Firefox, Chrome, Safari und Edge) der Fall.

Beachten Sie: Da getSelectedText das gesamte Dokument anspricht, können Sie auch die Überschrift und den anschließenden Absatz selektieren - im fertigen Editor wird dies geändert.

Range

Eine Textmarkierung kann als eine Range (englisch für Bereich, Ausschnitt, Spanne) dargestellt werden, die – vereinfacht gesagt – von einem Punkt im Dokument bis zu einem anderen reicht.[3] Bekanntlich betrachtet das DOM ein Dokument als Baumstruktur bestehend aus Knoten, vor allem Element- und Textknoten. Eine solche Range kann in einem Textknoten beginnen und in demselben oder einem anderen enden. Sie kann somit mehrere Knoten und deren (Teil-)Inhalt umfassen.[4]

Einfügen von HTML-Markup

Der selektierte Text soll nun durch die Toolbar-Buttons mit HTML-Markup passend formatiert werden. Falls kein Text selektiert wurde, soll an der Cursor-Position ein neuer Elementknoten erzeugt werden, in dem dann weitergeschrieben wird.

Im Originalartikel wurde der Inhalt der textarea ausgelesen, in die 3 Bestandteile Anfang, Selektierter Text und Ende getrennt und dann mit String-Methoden zusammen mit den Zeichenketten für Anfangs- und End-Tags verbunden:

 input.value = input.value.substr(0, start) + aTag + insText + eTag + input.value.substr(end);

So konnten aber auch verschachtelte Link-Elemente eingefügt, bzw. im HTML-Markup vorhandene Tags „zerschossen“ werden.

Die folgende Funktion fügt das gewählte Element ein, beendet aber das vorher gewählte und achtet in der Code-Ansicht darauf, dass es sich nicht um HTML-Tags handelt:

Einfügen von HTML-Markup ansehen …

ToDo (weitere ToDos)

Fortsetzung mit

  • appendChild
  • insertBefore
  • insertBefore + nextSibling
  • replaceChild
--Matthias Scharwies (Diskussion) 04:16, 29. Jul. 2020 (CEST)

Originalartikel

Einfügen von Inhalten in eine Textarea ansehen …
function insert(aTag, eTag) {
  var input = document.forms['formular'].elements['eingabe'];
  input.focus();
  /* für Internet Explorer */
  if(typeof document.selection != 'undefined') {
    /* Einfügen des Formatierungscodes */
    var range = document.selection.createRange();
    var insText = range.text;
    range.text = aTag + insText + eTag;
    /* Anpassen der Cursorposition */
    range = document.selection.createRange();
    if (insText.length == 0) {
      range.move('character', -eTag.length);
    } else {
      range.moveStart('character', aTag.length + insText.length + eTag.length);      
    }
    range.select();
  }
  /* für neuere auf Gecko basierende Browser */
  else if(typeof input.selectionStart != 'undefined')
  {
    /* Einfügen des Formatierungscodes */
    var start = input.selectionStart;
    var end = input.selectionEnd;
    var insText = input.value.substring(start, end);
    input.value = input.value.substr(0, start) + aTag + insText + eTag + input.value.substr(end);
    /* Anpassen der Cursorposition */
    var pos;
    if (insText.length == 0) {
      pos = start + aTag.length;
    } else {
      pos = start + aTag.length + insText.length + eTag.length;
    }
    input.selectionStart = pos;
    input.selectionEnd = pos;
  }
  /* für die übrigen Browser */
  else
  {
    /* Abfrage der Einfügeposition */
    var pos;
    var re = new RegExp('^[0-9]{0,3}$');
    while(!re.test(pos)) {
      pos = prompt("Einfügen an Position (0.." + input.value.length + "):", "0");
    }
    if(pos > input.value.length) {
      pos = input.value.length;
    }
    /* Einfügen des Formatierungscodes */
    var insText = prompt("Bitte geben Sie den zu formatierenden Text ein:");
    input.value = input.value.substr(0, pos) + aTag + insText + eTag + input.value.substr(pos);
  }
}

Im HTML-Bereich der Seite wird ein einfaches Formular mit einer Textarea definiert, die mit einem Standardtext vorbelegt ist, um die Funktionsweise des Scripts zu demonstrieren. Durch einen Klick auf den Button "Einfügen" wird die Funktion insert mit entsprechenden Parametern (Start-Tag und End-Tag) aufgerufen. Sie soll einen markierten Text als Link nach dem Schema [link]http://www.example.org[/link] formatieren bzw. ein leeres Link-Element einfügen, falls kein Text markiert wurde.

Die Funktion insert startet damit, in der Variablen input eine Referenz auf die Textarea zu speichern. Dazu werden der Name des Formulars (im Beispiel formular) und der Name der Textarea (im Beispiel eingabe) verwendet. Die eigentliche Funktion gliedert sich in drei Teile, die je nach den Fähigkeiten des Browsers bestimmte Techniken zum Einfügen der Formatierung verwenden. Zuerst wird das Objekt document.selection abgefragt, das nur vom Internet Explorer unterstützt wird. Falls es existiert, wird mit dessen Hilfe der Formatierungscode eingefügt und die Cursorposition angepasst. Falls document.selection nicht zur Verfügung steht, wird im nächsten Zweig geprüft, ob der Browser die Eigenschaft selectionStart unterstützt. Dies ist bei Browsern mit neuerer Gecko-Engine (z. B. Firefox, Chrome und Safari) der Fall.

Für den Fall, dass weder document.selection noch selectionStart bekannt sind, enthält der letzte Zweig den Code für alle anderen Browser (insbesondere Opera). In diesen Browsern besteht keine Möglichkeit, den markierten Text oder die Cursorposition in der Textarea auszulesen. Sowohl die Einfügeposition als auch der zu formatierende Text müssen daher mit Hilfe von window.prompt() abgefragt werden.

Beachten Sie: Das Script können Sie universell einsetzen. Die einzigen erforderlichen Parameter sind der Code, der vor der markierten Stelle eingefügt werden soll, und der Code, der nach der markierten Stelle eingefügt werden soll. Der einzufügende Code kann auch in einem HTML-Element bestehen. Sie können die insert-Funktion beispielsweise mit insert('<strong>', '<\/strong>' ) aufrufen, um ein strong-Element einzufügen, welches den Text hervorhebt. Es ist ebenfalls möglich, einteilige Codes an der Cursorposition einzufügen, etwa besondere Zeichen oder Emoticons. Übergeben Sie der Funktion in diesem Fall einfach einen leeren String als zweiten Parameter, zum Beispiel insert(' :-) ', ).

Über die Methode range.move() im Internet Explorer bzw. die selectionStart und selectionEnd in Browsern mit Gecko-Engine wird die Cursorposition nach dem Einfügen angepasst und gegebenenfalls die Markierung aufgehoben. Wenn vorher Text markiert wurde und in den Formatierungscode eingebettet wurde, dann wird der Cursor nach dem End-Tag platziert. Wenn kein Text markiert wurde und leerer Formatierungscode eingefügt wird, wird der Cursor zwischen dem Start- und dem End-Tag platziert. Sie können diese Korrekturen verhindern, indem Sie die entsprechenden Teile in der Funktion herausnehmen. Natürlich können Sie diese auch an Ihre Bedürfnisse anpassen.

Das Abfragen der Einfügeposition durch das prompt() -Fenster im dritten Teil der Funktion verwirrt den Anwender möglicherweise. Stattdessen können Sie den Formatierungscode automatisch an das Ende des eingegebenen Textes einfügen lassen. Ersetzen Sie dazu die Zeilen, die sich mit der Abfrage von pos beschäftigen, einfach durch den Befehl var pos = input.value.length; .

Mithilfe eines regulären Ausdrucks wird überprüft, ob die eingegebene Einfügeposition eine Zahl ist. Der reguläre Ausdruck wurde mit der eher unüblichen langen Schreibweise new RegExp('^[0-9]{0,3}$') erzeugt, damit auch der Browser Opera 5.x den Code versteht. Diese Schreibweise ist gleichwertig zur kürzeren /^[0-9]{0,3}$/ .

Fazit

Das Schreiben eines eigenen Rich Text-Editors ist einfach, schwierig wird es jedoch in der anschließenden Feinarbeit.

Ein Problem ist die richtige Bearbeitung ausgewählten Codes mit bereits bestehenden HTML-Elementen.[5] Solche Überlappungen sauber zu erkennen ist absolut nicht trivial.

Ein weiteres Problem ist das Abräumen unnötigen Markups, wenn Formatierungen entfernt werden.

Aus diesem Grund ist es zu empfehlen, anstelle einer eigenen Programmierung fertige WYSIWYG-Editoren einzubinden:

Beachten Sie:
Solchermaßen formatierter Text könnte auch „schädlichen“ Code in Form von script-Elementen oder iframes enthalten. Deshalb muss er nach dem Senden und vor dem Speichern auf dem Server auf jeden Fall serverseitig validiert werden.

Siehe auch

Quellen

  1. MDN: Making content editable
    • inkl. A simple but complete rich text editor
  2. W3C: HTML Editing APIs Work in Progress — Last Update 13 February 2014
    The features documented herein are obsolete. Authors should not use these features directly, but instead use JavaScript editing libraries. The features described in this document are not implemented consistently or fully by user agents, and it is not expected that this will change in the foreseeable future.
  3. W3C: DOM 2 Range
  4. Einen guten Einstieg zum Thema Ranges bietet Peter-Paul Kochs Introduction to Range
  5. SELF-Forum: WYSISYG-Editor: execCommand von Rolf B. vom 20.07.2020