Laura: deep copy eines Range-Objektes

Hallo,

ich habe Folgendes vor:
  1. über var range = window.getSelection().getRangeAt(0) die selection wrappen mit range.surroundContents(wrapper);
  2. dies soll rückgängig gemacht werden können (Strg+Z), indem der wrapper durch wrapper.firstChild ersetzt und mit wrapper.parentNode.normalize() die Zerstückelung der textNodes wieder aufgehoben wird;
  3. das Rückgängig-Machen soll rückgängig gemacht werden können (Strg+Y), indem das Range-Objekt zuvor abgespeichert und nun wieder verwendet wird.

Das Problem:
Ich müsste eine deep copy von range abspeichern - einfach var copy = range oder var copy = range.cloneRange() bringt nicht das gewünschte Resultat, weil hier per Referenz kopiert wird und der gewünschte Zustand von range durch surroundContents und collapse nicht erhalten bleibt.

Stark vereinfachtes Bsp.:

  
var ranges = [];  
  
function wrap(range, wrapper) {  
  var clone = ?????;  
  ranges.push(clone);  
  range.surroundContents(wrapper);  
  range.collapse(false);  
}  
  
function unwrap(wrapper) {  
  var parent = wrapper.parentNode;  
  parent.replaceChild(wrapper.fistChild, wrapper);  
  parent.normalize();  
}  
  
function rewrap() {  
  wrap(ranges.pop(), wrapper);  
}  

Ohne normalize() bliebe die textNode erhalten und ich bräuchte das Range-Objekt gar nicht wiederverwenden, aber leider kann ich darauf nicht verzichten.

  1. Vielleicht ist Crockfords beget-Funktion etwas für dich

      
    if (typeof Object.beget !== 'function') {  
        Object.beget = function(o) {  
            var F = function() {};  
            F.prototype = o;  
            return new F();  
        };  
    }  
    
    

    Hier noch [http://jsfiddle.net/e27b2/1/](ein Beispiel) zur Verwendung.

    1. Hier noch [http://jsfiddle.net/e27b2/1/](ein Beispiel) zur Verwendung.

      Was war denn da mit mir los?

      Korrigierter Link

      1. Vielen lieben Dank!
        Sorry, dass ich erst jetzt antworte; musste unerwartet nochmal aus'm Haus^^
        Liebe Grüße

    2. [latex]Mae  govannen![/latex]

      Vielleicht ist Crockfords beget-Funktion etwas für dich

      Eher nicht:

      var o1, o2;  
      o1 = {a:1, b:2, c:3};  
      o2 = Object.beget(o1);  
      console.log(o2.b); // 2  
      o1.b = 6;  
      console.log(o2.b); // 6  OOPS!
      

      Stur lächeln und winken, Männer!
      Kai

      --
      It all began when I went on a tour, hoping to find some furniture
       Followed a sign saying "Beautiful Chest", led to a lady who showed me her best)
      SelfHTML-Forum-Stylesheet
      1. Eher nicht:

        var o1, o2;

        o1 = {a:1, b:2, c:3};
        o2 = Object.beget(o1);
        console.log(o2.b); // 2
        o1.b = 6;
        console.log(o2.b); // 6  OOPS!

          
        Das ist interessant, wenn ich o2.b vorher nämlich mit einem Wert belege verhält es sich anders:  
          
        ~~~javascript
          
         o1 = {a:1, b:2, c:3};  
         o2 = Object.beget(o1);  
         o2.b = 2;  
         console.log(o2.b); // 2  
         o1.b = 6;  
         console.log(o2.b); // 2  
        
        

        Kann mir das jemand erklären oder ein Stichwort nennen?

        1. Das ist interessant, wenn ich o2.b vorher nämlich mit einem Wert belege verhält es sich anders:

          o1 = {a:1, b:2, c:3};
          o2 = Object.beget(o1);
          o2.b = 2;
          console.log(o2.b); // 2
          o1.b = 6;
          console.log(o2.b); // 2

          
          >   
          > Kann mir das jemand erklären oder ein Stichwort nennen?  
            
          o2.b "verweist" ohne das explizite Setzen von o2.b=2 auf die property b im prototype von o2. dieser prototype ist eine kopie per referenz von/auf o1 -> Ändere ich o1, so auch o2.b.  
          Im chain wird bei dem Aufruf console.log(o2.b) zunächst b in den "privaten"/"eigenen" properties von dem durch den Konstruktor erzeugten Objekt gesucht und dann erst im prototype.  
          Wird also o2.b definiert, so ist b eine property von o2 "direkt" und nicht vom prototype und wird gefunden, bevor überhaupt im chain weiter gegraben werden müsste. Die Suche endet mit dem Fund und führt nicht weiter zum prototype.  
          Setzt man nicht o2.b = 2, so wird keine eigene property gefunden, sondern b wird erst im prototype gefunden.  
            
          Lieben Gruß
          
          1. o2.b "verweist" ohne das explizite Setzen von o2.b=2 auf die property b im prototype von o2. dieser prototype ist eine kopie per referenz von/auf o1 -> Ändere ich o1, so auch o2.b.
            Im chain wird bei dem Aufruf console.log(o2.b) zunächst b in den "privaten"/"eigenen" properties von dem durch den Konstruktor erzeugten Objekt gesucht und dann erst im prototype.
            Wird also o2.b definiert, so ist b eine property von o2 "direkt" und nicht vom prototype und wird gefunden, bevor überhaupt im chain weiter gegraben werden müsste. Die Suche endet mit dem Fund und führt nicht weiter zum prototype.

            Ah hier passiert die Magie. Die Möglichkeit, dass die Property b für o2 neu angelegt wird hatte ich ausgeschlossen, weil ich dachte, dass dadurch der descriptor der Eigenschaft flöten geht. Ein einfacher Test beweist das Gegenteil.

            Beim setzen von o2.b wird also auch in der prototype-chain nachgeschaut, um den descriptor zu kopieren. Damit hab ich nicht gerechnet, aber ergibt bei weiterenm Nachdenken Sinn.

            Setzt man nicht o2.b = 2, so wird keine eigene property gefunden, sondern b wird erst im prototype gefunden.

        2. [latex]Mae  govannen![/latex]

          Das ist interessant, wenn ich o2.b vorher nämlich mit einem Wert belege verhält es sich anders:

          Jepp, „Laura“ hat es ja bereits erkläret.

          Kann mir das jemand erklären oder ein Stichwort nennen?

          Zusätzlich: Molily hat das dahinterstehende Prinzip anhand des Glasplatten-Modells recht anschaulich dargestellt

          Stur lächeln und winken, Männer!
          Kai

          --
          It all began when I went on a tour, hoping to find some furniture
           Followed a sign saying "Beautiful Chest", led to a lady who showed me her best)
          SelfHTML-Forum-Stylesheet
          1. [latex]Mae  govannen![/latex]

            Das ist interessant, wenn ich o2.b vorher nämlich mit einem Wert belege verhält es sich anders:

            Jepp, „Laura“ hat es ja bereits erkläret.

            Kann mir das jemand erklären oder ein Stichwort nennen?

            Zusätzlich: Molily hat das dahinterstehende Prinzip anhand des Glasplatten-Modells recht anschaulich dargestellt

            Aus der Metapher geht leider nicht hervor, dass beim Überschreiben einer Eigenschaft der descriptor von etwaigen Namensvettern in der Prototype-Kette geerbt wird. Siehe hier und hier. Das ist insbesondere dann interessant, wenn configurable auf false gesetzt wurde.

              
            var o1,o2;  
              
            o1 = {};  
              
            Object.defineProperty(o1,"a",{  
                set : function(val){ this.val = val + 1; },  
                get : function(){ return this.val; }  
            });  
              
            o1.a = 0;  
            console.log(o1.a); // 1  
              
            o2 = Object.beget(o1);  
              
            o2.a = 10;  
              
            log(o2.a);  
            console.log(o2.a); // 11  
            
            
            1. Aus der Metapher geht leider nicht hervor, dass beim Überschreiben einer Eigenschaft der descriptor von etwaigen Namensvettern in der Prototype-Kette geerbt wird.

              Das klingt so, als würde beim neuen Objekt ein Property mit diesem Descriptor angelegt, was aber nicht der Fall ist. Den Fall, den du beschreibst, kann man nicht so verallgemeinern. Enumerable, configurable oder writable vererben sich keinesfalls auf Eigenschaften.

              var o1,o2;

              o1 = {};

              Object.defineProperty(o1,"a",{
                  set : function(val){ this.val = val + 1; },
                  get : function(){ return this.val; }
              });

              o1.a = 0;
              console.log(o1.a); // 1

              o2 = Object.beget(o1);

              o2.a = 10;

              log(o2.a);
              console.log(o2.a); // 11

                
              Das ist klar, hier wird an o2 auch nie eine Property a angelegt.  
                
              o2.a = löst auf den Setter am Prototyp auf. Der holt sich val über die Prototype-Chain und erzeugt eine Property val an o2. o2.a löst dann ebenfalls auf den Getter am Prototyp auf.  
                
              Wenn man einen vererbten Setter am lokalen Objekt überschreiben will, kann man selbstverständlich nicht o.setter = … verwenden, denn das ruft ihn nur auf. Dazu bräuchte man schon einen weiteren Descriptor, der einen value anstatt einen setter angibt.  
                
              Mathias
              
              -- 
              [Chaplin - An Application Architecture Using Backbone.js](https://github.com/chaplinjs/chaplin)
              
  2. Hab's jetzt so gemacht:

    function wrap(range, wrapper) {
      range.surroundContents(wrapper);
      var clone = document.createRange();
      clone.selectNode(wrapper);
      ranges.push(clone);
      range.collapse(false);
    }

      
    Vermutlich die einfachste und eleganteste Lösung für diesen Fall, aber doch  
    unbefriedigend, dass ich keine simple Methode sehe, einen "Zustand" des  
    Range-Objektes "einzufrieren".  
      
    `console.log(Lib.copy(true, range))`{:.language-javascript} ergibt:  
     "[unsupported: no toString() function in type object]"  
      
    ~~~javascript
      
    // copy ist hier eine Methode eines Objekt-Literals, ebenso wie  
    // isBoolean, isArray und isLiteral  
    var Lib = {  
    copy: function(deep, obj, copy) {  
      copy || (copy = {});  
      var isBoolean = this.isBoolean(deep)  
      , isArray = false;  
      obj = (isBoolean) ? obj : deep;  
      deep = (isBoolean) ? deep : false;  
      for (var m in obj) {  
        if (deep && obj[m] && (this.isLiteral(obj[m]) || (isArray = this.isArray(obj[m])))) {  
          if (isArray) { copy[m] = obj[m].slice(0);  
          } else { copy[m] = this.copy(deep, obj[m]); }  
        } else { copy[m] = obj[m]; }  
      }  
      return copy;  
    }  
    //,...  
    }  
    
    
  3. So, hier mein letzter (und diesmal funktionierender) Ansatz - für den Fall,
    dass jemand ein ähnliches Problem hat:

    Stark vereinfachtes Bsp.:

      
    var copies = [],  
        parents = [];  
      
    function wrap(range, wrapper) {  
      // kann man noch optimieren; worauf es ankommt, ist, die properties  
      // startOffset, endOffset (und ggf. commonAncestorContainer etc.) zu  
      // sichern, weil diese in range selbst sich ändern durch bspl.  
      // surroundContents oder collapse  
      copies.push(eineDeepCopyFunction(range));  
      range.surroundContents(wrapper);  
      // dass ich hier in globale Arrays pushe, um die nötigen Infos zu retten,  
      // liegt am vereinfachten Bsp. - ist natürlich unschön  
      parents.push(wrapper.parentNode);  
      range.collapse(false);  
    }  
      
    function unwrap(wrapper) {  
      var parent = wrapper.parentNode;  
      parent.replaceChild(wrapper.fistChild, wrapper);  
      parent.normalize();  
    }  
      
    function rewrap(wrapper) {  
      // die copy ist kein Ränge-Objekt, mit dem man einfach so weiterarbeiten  
      // könnte; benötige ein paar properties, um damit neue range zu generieren  
      var range = document.createRange(),  
          parent = parents.pop(),  
          copy = copies.pop();  
      range.setStart(parent, copy.startOffset);  
      range.setEnd(parent, copy.endOffset);  
      wrap(range, wrapper);  
    }  
    
    
  4. Hallo,

    var copy = range.cloneRange() bringt nicht das gewünschte Resultat, weil hier per Referenz kopiert wird und der gewünschte Zustand von range durch surroundContents und collapse nicht erhalten bleibt.

    Habe schon lange nicht mehr mit Ranges gearbeitet, daher eine dumme Frage: Was ist der Sinn von cloneRange, wenn sich der Klon verändert, wenn sich das Original ändert? Müsste cloneRange nicht eigentlich das Mittel der Wahl sein?

    Mathias

    1. Habe schon lange nicht mehr mit Ranges gearbeitet, daher eine dumme Frage: Was ist der Sinn von cloneRange, wenn sich der Klon verändert, wenn sich das Original ändert? Müsste cloneRange nicht eigentlich das Mittel der Wahl sein?

      Ohje...
      habe einen ziemlich dämlichen Denkfehler begangen:
      In folgendem Code wird mithilfe der range das HTML-Element manipuliert, auf das sich ja auch der clone bezieht. Ändere ich also mithilfe von range die HTML, so ändert sich range selbst und somit NATÜRLICH auch der clone...:

        
      // selektiere Wort "Grünkohl":  
      function wrap() {  
        var selection = window.getSelection(),  
            range = selection.getRangeAt(0),  
            clone = range.cloneRange(),  
            wrapper = document.createElement('span');  
      console.log(clone.toString()); // Grünkohl  
        wrapper.style.background = "yellow";  
        range.surroundContents(wrapper);  
      console.log(clone.toString()); // (Leerstring)  
        range.collapse(true);  
      }  
        
      document.addEventListener("mouseup", wrap, false);  
      
      

      Lässt man range.surroundContents(wrapper) weg und prüft mit console.log nach collapse, dann sieht man auch, dass es sich
      tatsächlich um einen clone handelt:

        
      console.log(range.toString()); // Grünkohl  
      console.log(clone.toString()); // Grünkohl  
        range.collapse(true);  
      console.log(range.toString()); // (Leerstring)  
      console.log(clone.toString()); // Grünkohl  
      
      

      MDN:
      "clone is copied by value, not reference, so a change in either Range does not effect the other."

      Lieben Gruß!