Events und Closures

vorige Präsentation: Funktion Aufrufen | zurück zum Buch-Kapitel [esc] | Nächste Präsentation Prototypen + Vererbung

Dieses Kapitel ist ein Tutorial zu folgenden Projekt: Ein SVG-Editor soll programmiert werden, mit dem man Kreise und Rechtecke im Browser erzeugen und positioniern kann.

An diesem Beipiel wird die Behandlung von Event und die Notwendigkeit von Closures erläutert.

Der Fokus in diesem Kapitel liegt auf der Drag und Drop Funktionalität.

Die Ausgangslage

Dieses Programm wird umfangreich. Deswegen werden von Anfang an Strukturen zur Wiederverwertung von Code gelegt:

Für den Editor, und für Rechtecke (Rectangles) und Kreise (Circles) werden jeweils Konstruktor-Funktionen angelegt, mit der Editor-, Rechteck-, und Kreis-Objekte erzeugt werden können.

Editor

Die Konstruktur-Funktion des Editor Objekts nimmt folgende Argumente

Das Editor Objekt hat folgende Attribute:

Das Editor Objekt hat folgende Methoden

Javascript Code Konstruktor

function Editor( css_selector, w, h ) {
  this.renderlist = [];
  this.dom = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  this.dom.setAttribute("width", w || 600);
  this.dom.setAttribute("height", h || 600);
  emptyAndAppendChild( document.querySelector( css_selector ), this.dom );
  return this;
}

Editor.prototype.rectangle = function(  x,y,w,h,att, text ) {
  var o;
  o = new Rectangle( x,y,w,h, att, this.dom );
  this.renderlist.push(o);
  return o;
}

Editor.prototype.circle = function( x,y,r,att, text ) {
  var o;
  o = new Circle( x,y,r,att, this.dom );
  this.renderlist.push(o);
  return o;
}

Circle und Rectangle

Circle und Rectangle funktionieren sehr ähnlich, deswegen werden sie hier gemeinsam beschreiben. Als SVG-Elemente betrachtet gibt es größere Unterschiede: so wird die Position eines <circles> über die Attribute cx und cy gesetzt, die Position eines <rect> über x und y. Diese Unterschiede werden durch die neu erstellen Javascript-Objekte möglichst verborgen.

Die Signatur der Konstruktur-Funktion sind:

Fast alle Argumente sind also gleich und gleich bedeutend. Der einzige Unterschied: Die Größe des Rechtecks wird über Breite w und Höhe h definiert, die Größe des Kreis über den Radius r. Der Aufruf der Konstruktors erzeugt das SVG-Element, fügt es in den canvas ein, setzt die Attribute und fügt geeignete Event-Handler für drag-and-drop ein.

Die anderen Argumente sind:

Das Rectangle und das Circle Objekt haben folgende Attribute:

Das Rectangle und das Circle Objekt haben folgende Methoden

Javascript Code Konstruktor für Kreis

var nextCircleId = idGenerator( "cicle" );

function Circle( x,y,r,att, canvas ) {
  var shape,a;

  this.id = nextCircleId();
  this.canvas = canvas;

  //   <circle>
  this.dom = document.createElementNS("http://www.w3.org/2000/svg", "circle");
  this.dom.setAttribute("id", this.id);
  this.dom.setAttribute("cx", x);
  this.dom.setAttribute("cy", y);
  this.dom.setAttribute("r",  r);
  addAttributes(this.dom, att, {"fill": "white", "stroke": "black", "stroke-width": 1});
  canvas.appendChild(this.dom);
  
  addDragability( this, this.dom );
}
Circle.prototype.getPosition = function () {
  return {  x: parseInt( this.dom.getAttribute( "cx" ) ), y : parseInt( this.dom.getAttribute("cy") ) };
};
Circle.prototype.setPosition = function ( position ) {
  this.dom.setAttribute("cx", position.x );
  this.dom.setAttribute("cy", position.y );
};

Drag and Drop

Wie funktioniert Drag-and-Drop? Wir haben die Events mousedown, mouseup und mousemove zur Verfügung.

Mausposition

Das Auslesen der aktuellen Mous-Position ist leider nicht einheitlich implementiert. Hier eine Implementierung die in allen Browsern funktionieren sollte:

Javascript Code auslesen der Maus-Position aus einem Event-Objekt

function cursorPoint(e) {
  var posx = 0;
  var posy = 0;
  if (!e) var e = window.event;
  if (e.pageX || e.pageY)   {
    posx = e.pageX;
    posy = e.pageY;
  }
  else if (e.clientX || e.clientY)  {
    posx = e.clientX + document.body.scrollLeft
      + document.documentElement.scrollLeft;
    posy = e.clientY + document.body.scrollTop
      + document.documentElement.scrollTop;
  }
  // posx and posy contain the mouse position relative to the document
  // Do something with this information
  return { 'x': posx, 'y': posy  };
}

Wiederholung: Event-Listener

Wenn wir Event-Listener-Funktionen für diese Events definieren haben wir der Funktion jeweils Zugriff auf ein Element der DOM, das durch das Event betroffen ist.

Also zum Beispiel:

Javascript Code Event Listener

function onMouseDown(e) {
  console.log("Mouse down. this is " + this.id + " event is " + e);
}

element.addEventListener( 'mousedown', onMouseDown);

Wenn die Funktion onMouseDown aufgerufen wird enthält this das angeklickte Element, und e enthält ein Event-Objekt mit weiteren Details.

Problemstellung: vom DOM-Element zum Javascript-Objekt

In unserem Javascript-Programm wird ein Kreis nicht alleine durch die SVG-Node <circle> repräsentiert, sondern durch ein Javascript-Objekt das mit dem Konstruktor Circle erzeugt wurde.

Wie kann die Listener-Funktion Zugriff zu diesem Javascript-Objekt erhalten?

Ein Versuch wäre, das Javascript-Objekt im SVG-Element zu speichern. zum Beispiel in einem Data-Attribut. In HTML5 darf man ja zu jedem Tag / Element beliebige neue Attribute dazu erfinden, solange der Name des Attributs mit “data-“ beginnt.

Html Code data-Attribute in HTML5

<h1 data-geheim="wichtig daten">Normaler Überschrift</h1>
....
<circle data-weight="10kg">

Ein Data-Attribut könnte man mit Javascript setzen. Hier ein Versuch:

Javascript Code Javascript-Objekte in einem Data-Attribut in der DOM speichern - geht nicht

// Versuch ein Objekt zu speichern
c = document.getElementById("circle_1");
c.setAttribute("data-myobject", { color: 'red', no: 42 } ); 

// auslesen der Daten
o = c.getAttribute("data-myobject");    
typeof o        // ergibt "string"
console.log(o)  // ergibt "[object Object]"

Beim Speichern in der DOM wurde also mein Objekt in einen String umgewandelt, und nur der String wurde gespeichert.

Es ist nicht möglich beliebige Javascript-Objekte in der DOM zu speicher!

Lösungs-Ansatz mit Closure

Die übliche Lösung in Javascript ist die Verwendung von Closures: die Event-Listener-Funktion ist eine Closure, die das Javascript-Objekt kennt, und deswegen darauf Zugriff hat.

Javascript Code Event-Listener-Funktion als Closure

function addDragability( jsobj, element ) {

  function onMouseDown(e) {
    console.log("Mouse down");
    console.log("my element is " + this);
    console.log("my javascript object is " + jsobj);
    console.log("my event is " + e);
  }

  element.addEventListener( 'mousedown', onMouseDown);
}

addDragability( jsobj, element );
addDragability( other_jsobj, other_element );

Umsetzung

Wenn man die Beschreibung von Drag-and-Drop nochmal betrachtet …

und das nun in Event-Listener umsetzen will kommt man zu folgender Struktur

Javascript Code Struktur der Event-Listener für Drag-and-Drop

function addDragability( jsobj, element ) {
  function onMove(e) {
    // die Differenz zwischen aktueller Maus-Position 
    // und der Start-Position berechnen. Das ergibt 
    // einen Translations-Vektor. 
    // Diesen Vektor zur Startposition des Objekts 
    // addieren, aktuelle Position des Objekts setzen
  }
  function onMouseDown(e) {
    // Drag-und-Drop beginnt: den richtigen Eventlistener 
    // für mousemove setzen
    // Die Position der Maus und des Objekt zu diesem 
    // Start-Zeitpunkt müssen gespeichert werden.
  }
  function onMouseUp(e) {
    // Drag-and-Drop beenden: nicht mehr gebrauchte 
    // event-listener entfernen
  }

  console.log( jsobj + " und damit " + element + " sind nun draggable");
ToDo fertigen Code zeigen - später

Events und Closures

vorige Präsentation: Funktion Aufrufen | zurück zum Buch-Kapitel [esc] | Nächste Präsentation Prototypen + Vererbung

/

#