Big Bene: Editierbare "Textfelder" in HTML

Hallo!

Ich möchte gern eine Seite erstellen, in der der Anwender Text im HTML-Format editieren kann - also so, daß er z. B. Text als "fett" definiert und dieser dann auch am Bildschirm so erscheint.
Es gibt solche Seiten, z.B. ckeditor, oder die HTML-Editoren bei GMX oder in Context-Management Systemen. Diese Seiten nutzen Javascript.
Ich bin ganz gut in Javascript, aber ich verstehe nicht, wie das funktioniert.
Man muß ja, wenn der Anwender einen Button anklickt, dem markierten Text ein Format zuweisen. Aber in Javascript kann man über selected bzw. getselected ja nur den Inhalt der Markierung auslesen, man erfährt nicht, welche Stelle im Gesamttext markiert ist...
Auch stellen die obengenannten Seiten den zu bearbeitenden Text offenbar in einem Textfeld dar (textarea), was die Bearbeitung durch den Anwender natürlich einfacher macht. Aber wie erzeugt man ein Textfeld, das HTML-Formate anezigt?

Ich bin mir natürlich darüber im klaren, daß die obengenannten Seiten extrem komplexe und umfangreiche Projekte sind, die von ganzen Programmierteams erstellt wurden. Es geht mir auch gar nicht darum, einen Editor mit allen möglichen Optionen zu programmieren, ich möchte nur wissen, wie das Problem, mit Javaskcipt auf den Text zuzugreifen, grundsätzlich gelöst wurde.

Danke und Grüße

  1. [...] ich möchte nur wissen, wie das Problem, mit Javaskcipt auf den Text zuzugreifen, grundsätzlich gelöst wurde.

    Interessiert dich, wie es technisch funktioniert oder wie du selbst nachbauen kannst?

    1. [...] ich möchte nur wissen, wie das Problem, mit Javaskcipt auf den Text zuzugreifen, grundsätzlich gelöst wurde.

      Interessiert dich, wie es technisch funktioniert oder wie du selbst nachbauen kannst?

      Wenn ich weiß, wie es technisch funktioniert, kann ich es hoffentlich nachbauen ;)

      1. Hi!

        [...] ich möchte nur wissen, wie das Problem, mit Javaskcipt auf den Text zuzugreifen, grundsätzlich gelöst wurde.

        Schau mal da: http://aktuell.de.selfhtml.org/artikel/javascript/textauswahl/ und da: http://aktuell.de.selfhtml.org/artikel/javascript/bbcode/

        Lo!

        1. Super, das ist schon mal sehr hilfreich!

      2. Lieber Big Bene,

        Wenn ich weiß, wie es technisch funktioniert, kann ich es hoffentlich nachbauen ;)

        dass das keine gute Idee ist, weil Du dann unendlich mit Browser-Quirks zu kämpfen hast, wird Dir spätestens dann bewusst, wenn Du Dir anschaust, seit wie langer Zeit solche RTEs wie (F)CKEditor oder TinyMCE schon entwickelt werden und welche unglaublichen Klimmzüge sie machen, um eine in allen Browsern gleichbleibende Benutzung zu gewährleisten.

        Schau einmal bei einem solchen Editor (ich bevorzuge TinyMCE) in die API-Dokumentation. Sie ist nicht umsonst so umfangreich, weil es eben extreme Mühen sind, die die Entwickler seit Jahren auf sich nehmen, damit das Teil in allen (unterstützten) Browsern zuverlässig tut.

        Es hat seinen Sinn, einen fertigen RTE zu benutzen!

        Liebe Grüße,

        Felix Riesterer.

        --
        ie:% br:> fl:| va:) ls:[ fo:) rl:| n4:? de:> ss:| ch:? js:) mo:} zu:)
    2. Interessiert dich, wie es technisch funktioniert oder wie du selbst nachbauen kannst?

      Glücklicherweise gibt es dazwischen meist keine große Differenz im Bereich HTML/CSS/JavaScript. Selbst riesige Bibliotheken sind mehr eine Ansammlung von trivialen Kleinigkeiten, die nur wegen Featuritis, Browserkompatibilität, Performance-Optimierungen, Pattern-Benutzung und kryptischem Coding-Style komplex aussehen. Inhärent komplex sind höchstens gewisse Algorithmen. Aber solche Logiken braucht man beim DOM-Scripting nur selten.

      Was RTE so komplex macht, sind lediglich die Browserunterschiede und das Problem, einheitlichen und vernünftigen HTML-Code damit zu produzieren. Davon abgesehen ist es recht trivial, man kann also schnell einen einfachen Prototyp aufsetzen, der erst einmal in einem Browser läuft.

      Mathias

  2. Man muß ja, wenn der Anwender einen Button anklickt, dem markierten Text ein Format zuweisen. Aber in Javascript kann man über selected bzw. getselected ja nur den Inhalt der Markierung auslesen, man erfährt nicht, welche Stelle im Gesamttext markiert ist...

    Das kann man durchaus auslesen über DOM Selection und Range (siehe auch https://developer.mozilla.org/en/DOM/selection und https://developer.mozilla.org/en/dom:range).

    Das Formatieren selbst wird meist mit document.execCommand (siehe auch HTML5) oder eben DOM-Operationen auf Basis von Selection/Range gelöst.

    Auch stellen die obengenannten Seiten den zu bearbeitenden Text offenbar in einem Textfeld dar (textarea), was die Bearbeitung durch den Anwender natürlich einfacher macht. Aber wie erzeugt man ein Textfeld, das HTML-Formate anezigt?

    Es ist keine textarea. Es ist ein normales HTML mit dem contentEditable-Flag bzw. ein iframe mit einem Dokument mit dem designMode-Flag. Schau dir das mal im DOM Inspector von Firebug oder Chrome/Safari an.

    Die textarea existiert höchstens als Fallback. Intern wird natürlich HTML-Code erzeugt.

    Wenn du mal nach »Lightweight RTE« (RTE = Rich text editing) oder ähnlichem suchst, findest du viele kleine überschaubare Scripte, die diese Basistechniken verwenden. Hier etwa ein Beispiel für jQuery: http://code.google.com/p/rte-light/source/browse/trunk/jquery.rte.js – Da wirst du execCommand, die Selection-API und designMode/contentEditable wiederfinden.

    Mathias

  3. Hallo und nochmal herzlichen Dank an alle! Ihr habt mir wirklich auf die Sprünge geholfen.

    Zu den Bedenken von Felix Riesterer:
    Ich will gar nicht die Arbeit der bisherigen Entwickler in Frage stellen oder etwas "neu" oder "besser" machen, aber ich bin halt ein notorischer Selbermacher und will auf jeden Fall verstehen, was auf meinem Server läuft. Ein kleines und weniger funktionales selbstgemachtes Tool ist mir lieber als ein großes, wunderbar funktionales und dazu noch kostenloses, das ich aber nicht verstehe und dessen Funktion ich einfach hinnehmen muß. Und da dies ja ein Autorenforum mit Themenbereich "Programmiertechnik" ist, denke ich, man kann für alle Interessierten durchaus mal erörtern, wie man RTE realisiert.
    Das soll jetzt keine Kritik an Deiner Auffassung sein - ich verstehe Deine Bedenken einfach als hilfreichen Tipp für die Praxis, und möchte hier nur erklären, warum ich es in diesem Fall trotzdem erstmal selbst versuche.

    So, nun meine nächste Frage. Schaut Euch mal folgenden Code an:
    (läuft nicht im Internet Explorer)

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">  
    <html>  
    <head>  
    	<title></title>  
    	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">  
      
      
    <script type="text/javascript">  
    <!--  
    [code lang=javascript]function insert(aTag, eTag) {  
      var input = document.getElementById("Blatt");  
      input.focus();  
         var rrange = window.getSelection ();  
        var startnode = rrange.anchorNode;  
         if (startnode == document.getElementById("Blatt"))  
          {alert ("im Blatt");}  
         else  
          {alert("anderswo");};  
      
        var start = rrange.anchorOffset;  
    alert(start);  
        var endnode = rrange.focusNode;  
        var ende = rrange.focusOffset;  
        var insText = rrange.toString.substring(start, ende);  
        alert (insText);  
        document.getElementById("Blatt").firstChild.replaceData(start, (ende-start), (aTag + insText + eTag));  
      
        /* 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;  
    }
    ~~~//-->  
    </script>  
      
      
    </head>  
      
    <body bgcolor=#cccccc>  
      
      
    		<table border=2 cellpadding="0px" cellspacing="0px" style="width:95%">  
      
    			<tr height="60px">  
    				<td align="center" style="background-color:#eeeeee;" name="Table" ID="Table">  
    					Test Test Test  
              <input type="button" value="Einf&uuml;gen" onClick="insert('[i]', '[/i]')">  
    				</td>  
    			</tr>  
    		</table>  
        <br>  
    	  
            <!-- \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  -->  
             <div ID="Blatt" name="Blatt" contenteditable="true"  style="overflow:scroll; height:400px; width:95%; border:4px inset grey; background-color:#ffffff">Hier Seitentext eingeben  
      
             </div>  
            <!-- \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*  -->  
      
    Test Test Test  
      
      
    </body>  
    </html>[/code]  
      
    Zur besseren Übersichtlichkeit habe ich hier nur den fraglichen Bereich wiedergegeben, im Originalscript steht noch eine Browserweiche und eigener Code für den Internetexplorer.  
      
    Das Problem:  
    Ich würde gerne feststellen in welchem Node die Markierung beginnt. Dies geschieht im Abschnitt:  
    ~~~javascript
        var startnode = rrange.anchorNode;  
         if (startnode == document.getElementById("Blatt"))  
          {alert ("im Blatt");}  
         else  
          {alert("anderswo");};  
    
    

    (zum Test wird hier einfach ein Alert ausgegeben).
    Nun sollte eigentlich im ersten Fall, wenn die Markierung innerhalb der Schreibfläche (Div mit ID "Blatt") liegt, die Abfrage ein positives Ergebnis haben, also der Alert "Im Blatt" ausgegeben werden, falls die Markierung anderswo auf der Seite liegt, sollte die Abfrage negativ sein und der Alert "anderswo" ausgegeben werden. Tatsächlich verhält sich das Script, wenn man es im Browser testet, aber genau umgekehrt.
    Wahrscheinlich ist es ein ganz einfacher, dummer Fehler, aber ich stehe wirklich auf dem Schlauch...
    Im Moment sieht das wie ein nebensächliches Problem aus, aber später im Script wird die Abfrage des Nodes mit der Markierung noch ziemlich wichtig.

    Vielen Dank und beste Grüße

    Bene

    1. Hallo,

      anscheinend bringst du da einiges durcheinander: RTE mit contenteditable/execCommand einerseits und das Einfügen von Markup-Tags wie [i]...[/i] in eine normale Textarea andererseits. Du hast beides vermischt, ich wüsste nicht, welchen Zweck das dient.

      Wenn du nur [i]...[/i] einfügen willst, ohne dass sich die sichtbare Formatierung ändert, brauchst du contenteditable nicht. Da kannst du direkt das Beispiel aus http://aktuell.de.selfhtml.org/artikel/javascript/bbcode/ verwenden.

      contenteditable ergibt nur Sinn, wenn du mit execCommand auch wirklich den markierten Text formatierst, z.B. kursiv mit execCommand('italic'), anstatt nur Markup wie [i]...[/i] einzufügen.

      Beides lässt sich nicht kombinieren bzw. man kann ersteres natürlich auch mit contenteditable umsetzen, aber das ist weitaus schwieriger. Daher müsstest du dich mal entscheiden, was du willst.

      Mathias

      1. Ich weiß schon, was ich will - nämlich, daß der Benutzer den editierbaren Text in formatierter Form sieht. Ich brauche also durchaus contenteditable.
        Ich habe nur anfangs zum Ausprobieren eine textarea nach dem Beispiels von
        http://aktuell.de.selfhtml.org/artikel/javascript/bbcode/
        benutzt.

    2. Lieber Big Bene,

      aber ich bin halt ein notorischer Selbermacher und will auf jeden Fall verstehen, was auf meinem Server läuft.

      damit kann ich sehr gut leben, denn ich sehe eigene Eigenschaften von mir darin. ;-) Als ich seinerzeit etwas ähnliches probierte, musste ich mit den Tücken der Browser kämpfen - und habe mich doch nur auf IE und FF beschränkt. Als ich für mein Plugin tiefer in TinyMCE einsteigen musste, habe ich erkannt, dass es zwar nett ist, die generelle Funktionsweise zu begreifen, aber dass das Ausbügeln der Browserquirks einfach die Zeit nicht wert ist und man besser auf eine ausgereifte fertige Lösung zurückgreift.

      Viel Spaß beim Experimentieren! Ich weiß, es macht einfach Spaß Dinge selber zu können...

      Liebe Grüße,

      Felix Riesterer.

      --
      ie:% br:> fl:| va:) ls:[ fo:) rl:| n4:? de:> ss:| ch:? js:) mo:} zu:)
    3. var rrange = window.getSelection ();
          var startnode = rrange.anchorNode;
           if (startnode == document.getElementById("Blatt"))
            {alert ("im Blatt");}
           else
            {alert("anderswo");};

      Wenn die Markierung innerhalb von Blatt ist, dann enthält anchorNode den , welcher Kind von <div id="Blatt"></div> ist.

      var start = rrange.anchorOffset;
      alert(start);
          var endnode = rrange.focusNode;
          var ende = rrange.focusOffset;
          var insText = rrange.toString.substring(start, ende);

      Hier fehlen die () zum Aufruf der toString-Funktion.

      document.getElementById("Blatt").firstChild.replaceData(start, (ende-start), (aTag + insText + eTag));

      Hier nutzt du ja auch ausschließlich den einen Textknoten.

      Das ganze wird übrigens nicht funktionieren, wenn Blatt etwas anderes als bloß einen Textknoten enthält oder die Markierung darüber hinaus geht. Daher wie gesagt: Entweder es reicht eine Textarea, dann sparst du dir viel Gefummel mit der Selection-API (IE ausgenommen). Oder du willst wirkliches WYSIWYG, dann müsstest du davon ausgehen, dass Markierungen knotenübergreifend sein können und execCommand verwenden.

      /* 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;

      Das wird nicht funktionieren, du arbeitest nicht mit einem input-Element.

      Mathias

      1. Wenn die Markierung innerhalb von Blatt ist, dann enthält anchorNode den , welcher Kind von <div id="Blatt"></div> ist.

        Huch, da ist das wichtigste Wort geschluckt worden:

        Wenn die Markierung innerhalb von Blatt ist, dann enthält anchorNode den TEXTKNOTEN, welcher Kind von <div id="Blatt"></div> ist.

        1. Wie oben gesagt, das scheint der entscheidende Fehler in meinem Script zu sein.

          Grüße und Danke nochmal!

      2. Wenn die Markierung innerhalb von Blatt ist, dann enthält anchorNode den , welcher Kind von <div id="Blatt"></div> ist.

        Super, das scheint der Fehler zu sein! Danke nochmal!

        Hier fehlen die () zum Aufruf der toString-Funktion.

        »»

        OK

        document.getElementById("Blatt").firstChild.replaceData(start, (ende-start), (aTag + insText + eTag));

        Hier nutzt du ja auch ausschließlich den einen Textknoten.

        Ja, um den geht es auch.

        Das ganze wird übrigens nicht funktionieren, wenn Blatt etwas anderes als bloß einen Textknoten enthält oder die Markierung darüber hinaus geht.  ...  Oder du willst wirkliches WYSIWYG, dann müsstest du davon ausgehen, dass Markierungen knotenübergreifend sein können und execCommand verwenden.

        Das ist unter anderem der Sinn der Abfrage von anchorNode und focusNode: Wenn die Markierung ganz oder teilweise außerhalb von "Blatt" liegt, soll die Funktion nicht ausgeführt werden.

        Das wird nicht funktionieren, du arbeitest nicht mit einem input-Element.

        Jo, du hast recht, mein Fehler. Ich hatte zuerst eine textarea, und diese mit der Variable input identifiziert. Dann habe ich das ganze auf ein contenteditable div umgestellt. Dabei habe ich diesen Abschnitt einfach vergessen - ich war ja schon vorher an der Abfrage des anchorNode hängengeblieben.

        1. Das ist unter anderem der Sinn der Abfrage von anchorNode und focusNode: Wenn die Markierung ganz oder teilweise außerhalb von "Blatt" liegt, soll die Funktion nicht ausgeführt werden.

          Vielleicht hilft dir dann dieses einfache Beispiel weiter.
          http://molily.de/temp/execcommand.html

          Wenn man mit execCommand arbeitet, dann ändert sich die Knotenstruktur, weil Elemente eingefügt und die Textknoten aufgesplittet werden. D.h. man muss generischer prüfen, ob die ausgewählten Knoten im RTE-Bereich liegen. Das geht gut mit compareDocumentPosition bzw. contains, siehe Quellcode.

          Das Beispiel funktioniert in Firefox 3.6, Safari 5.0.4, Chrome 10 sowie IE 8 & 9.

          Ich hab's vermutlich schon verlinkt, hier zwei Beispielscripte, die allgemein die Arbeit mit window.getSelection() und W3C DOM Ranges bzw. document.selection und Microsoft TextRanges illustrieren:
          http://molily.de/weblog/selectionmenu-copylink
          https://github.com/molily/selectionmenu

          Mathias