JavaScript/Tutorials/Drag and Drop

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Das Verschieben von Seitenelementen kann ein hübsches Feature Ihrer Homepage darstellen. Sie können Ihren Besuchern ermöglichen, z. B. Elemente mit Zusatzinformationen beliebig zu positionieren. Das folgende Drag-and-Drop-Script demonstriert Ihnen die Verfahrensweise.

Während über das Drag & Drop API das Verschieben nur auf vordefinierte Plätze möglich ist, kann mit dem hier vorgestellten Script ein Element frei bewegt werden.

Das Verschieben der Elemente teilt sich in drei Phasen auf:

  1. Down-Phase: Das Element wird mit Maus (Taste drücken), Finger (Touch), o.Ä., also mit dem Pointer ausgewählt.
  2. Move-Phase: Durch Bewegen des Pointers wird das Element an eine andere Stelle gezogen.
  3. Up-Phase: Durch Heben des Pointers, z. B. Maustaste lösen, wird die Aktion beendet.

In der ersten Phase werden das zu bewegende Element, dessen Position und die Position des Pointers ermittelt und gespeichert.

In der zweiten Phase wird dann beim Bewegen des Pointers dessen aktuelle Position mit den gespeicherten Werten verrechnet und das Element mit dem Pointer bewegt.

In der dritten Phase werden die gespeicherten Werte gelöscht und weiteres Bewegen so unterbunden.

Für die drei Phasen werden Eventhandler für Pointer-, Touch-, Maus- und Tastaturevents notiert.


Das HTML

Um dem Script mitzuteilen, welche Elemente beweglich sein sollen, muss ihnen die Klasse "draggable" gegeben werden. "draggable" ist im Script als Klasse für bewegliche Elemente gewählt worden. Die beweglichen Elemente dürfen auch weitere Elemente enthalten:

Beispiel
<div class="draggable"><p>Beweg mich</p><button class="close">X</button></div>

Das CSS

Um Elemente frei und aus dem Fluss genommen positionieren zu können, müssen diese absolute positioniert sein. Dieses kann im Script oder auch im CSS erfolgen.

Beispiel
.draggable { 
  position: absolute; 
}

Das Javascript

Ermitteln der Position eines Elements

Das Auslesen der Elementposition erfolgt über die Eigenschaften offsetLeft und offsetTop:

Beispiel
start = {x: dragele.offsetLeft, y: dragele.offsetTop};

Ermitteln der Position des Pointers

Für die Maus- und Pointerevents wird die Pointerposition über die Eigenschaften clientX und clientY des Eventobjekts ausgelesen. Bei Touchevents muss dieses über das erste Element des targetTouches-Arrays erfolgen. Die Funktion get_pointer_pos benötigt als Parameter das Eventobjekt.

Beispiel
function get_pointer_pos(e) {
	let posx=0, posy=0;
	if(touch_event && e.targetTouches && e.targetTouches[0] && e.targetTouches[0].clientX) {
		posx = e.targetTouches[0].clientX;
		posy = e.targetTouches[0].clientY;
	}
	else if(e.clientX) {
		posx = e.clientX ;
		posy = e.clientY ;
	}
	return {x: posx, y: posy};
} // get_pointer_pos

Die Eventmodelle

Es gibt drei Eventmodelle für die Behandlung von Maus- oder Touchevents. Da verschiedene Browser ein, zwei oder auch alle drei Modelle unterstützen, werden auch alle drei berücksichtigt. Es wird geprüft, welche Modelle vom Browser unterstützt werden und welches dann eingesetzt werden soll. Beim bevorzugten Pointer-Modell werden die Events pointerdown, pointermove und pointerup gewählt. Wird das Pointermodell nicht unterstützt, werden für das Touchmodell die Events touchstart, touchmove und touchend gewählt. Werden auch keine Touchevents unterstützt, kommen die Mausevents mousedown, mousemove und mouseup zum Einsatz. Um auch das Bewegen mit den Pfeiltasten zu ermöglichen, wird auch das keydown-Event berücksichtigt.

Statt an jedem bewegbaren Element Eventhandler für diese Events zu notieren, werden die Handler nur einmal am body-Element notiert. Da die Events aufsteigen (event bubbling), werden so auch die Events bei allen Elementen erfasst, und damit auch bei den bewegbaren.


Beispiel
// Prüfen, welche Eventmodelle unterstützt werden und welches verwendet werden soll
const pointer_event = ("PointerEvent" in window);
const touch_event = ("TouchEvent" in window) && !pointer_event;

// Alle Eventhandler notieren
if(pointer_event) {
	document.body.addEventListener("pointerdown",handle_down,false);
	document.body.addEventListener("pointermove",handle_move,false);
	document.body.addEventListener("pointerup",handle_up,false);
}
else if(touch_event) {
	document.body.addEventListener("touchstart",handle_down,false);
	document.body.addEventListener("touchmove",handle_move,false);
	document.body.addEventListener("touchend",handle_up,false);
}
else {
	document.body.addEventListener("mousedown",handle_down,false);
	document.body.addEventListener("mousemove",handle_move,false);
	document.body.addEventListener("mouseup",handle_up,false);
}

document.body.addEventListener("keydown",handle_keydown,false);

Die Eventhandler

In den Eventhandlern wird dann die Pointerposition ermittelt und danach die Funktion für die entsprechende Phase des Bewegens aufgerufen. Der Keydown-Handler ermittelt über e.keyCode, welche Taste gedrückt wurde, setzt die Verschiebewerte auf ±1 oder bei gedrückter Shift-Taste (e.shiftKey) auf ±10 und ruft dann die Funktionen für die drei Phasen auf. So simuliert der keydown-Handler pointerdown, pointermove und pointerup:

Beispiel
//Eventhandler für pointerdown, touchstart oder mousedown
function handle_down(e) { 
	const pos = get_pointer_pos(e);
	down(e,pos);
} // handle_down

//Eventhandler für pointermove, touchmove oder mousemove
function handle_move(e) {
	const pos = get_pointer_pos(e);
	move(e,pos);
} // handle_move

//Eventhandler für pointerup, touchend oder mouseup
function handle_up (e) {
	up(e);
} // handle_up

//Eventhandler für keydown
function handle_keydown(e) {
	const keyCode = e.keyCode;
	let xwert = 0, ywert = 0;
	if(keyCode && (keyCode==27 || keyCode==37 || keyCode==38 || keyCode==39 || keyCode==40)) { 
		let delta = e.shiftKey?10:1;
		down(e,{x: 0,y: 0});
		switch(keyCode){
			case 37: // links
				xwert = -delta;
				break;
			case 38: // rauf
				ywert = -delta;
				break;
			case 39: // rechts
				xwert = delta;
				break;
			case 40: // runter
				ywert = delta;
				break;
			case 27: // Escape
				esc();
				up(e);
				return;
				break;
		}
		move(e,{x: xwert,y:ywert});
		up(e);
	}
} // keydown

Die drei Phasen des Drag and Drop

Die erste Phase wird in der Funktion down bearbeitet, die vom Eventhandler handle_down aufgerufen wird. In dieser Funktion wird über die Funktion parent geprüft, ob und in welchem beweglichen Element das Event ausgelöst wurde. Dazu wird geprüft, ob es ein Elternelement mit dem entsprechenden Klassennamen gibt. Wenn es das nicht gibt, weil nicht in ein bewegbares Element geklickt wurde, passiert nichts. Sonst werden das bewegliche Element und die Positionen von Pointer und Element gespeichert. Zusätzlich wird der z-Index erhöht, damit das Element immer "oben" liegt, und der Fokus auf das Element gesetzt. Um ein Markieren der Elemente unter dem Pointer oder ein Scrollen der Seite zu verhindern, wird die Methode preventDefault() aufgerufen und für Pointerevents die touch-action auf "none"gesetzt:

Beispiel
// Vorfahrenelement mit Klasse classname suchen
function parent(child,classname) {
	if(child && "closest" in child) return child.closest("."+classname);
	let ele = child;
	while(ele) {
		if(ele.classList && ele.classList.contains(classname)) return ele;
		else ele = ele.parentElement;
	}
	return null;
} // parent

// Auswahl des Dragelements und Start der Dragaktion
function down(e,pos) {
	const target = parent(e.target,drag_class);
	if(target) {
		document.body.style.touchAction = "none";
		e.preventDefault();
		dragele = target;
		start = {x: dragele.offsetLeft, y: dragele.offsetTop};
		pos0 = pos;
		dragele.style.zIndex = ++zmax;
		dragele.focus();
	}
} // down

Die zweite Phase wird in der Funktion move bearbeitet, die vom Eventhandler handle_move aufgerufen wird. Beim Ziehen der Maus oder mit dem Finger (Pointermove) wird die aktuelle Pointerposition mit den Startpositionen vom Pointer und vom zu bewegendem Element verrechnet und das Element neu positioniert. Um ein Markieren der Elemente unter dem Pointer oder ein Scrollen der Seite zu verhindern, wird die Methode preventDefault() aufgerufen:


Beispiel
// Bewegen des Dragelements
function move(e,pos) {
	if(dragele) {
		e.preventDefault();
		dragele.style.left = (start.x + pos.x - pos0.x) + "px";
		dragele.style.top =  (start.y + pos.y - pos0.y) + "px";
	}
} // move

Die dritte Phase wird in der Funktion up bearbeitet, die vom Eventhandler handle_up aufgerufen wird. Beim Lösen der Maustaste oder Heben des Fingers (Pointerup) wird die Variable dragele gelöscht und für Pointerevents die touch-action wieder auf default gesetzt:

Beispiel
// Ende der Aktion
function up(e) { 
	if(dragele) {
		dragele = null;
		document.body.style.touchAction = "auto";
	}
} // up

Beim Drücken der Escape-Taste wird dem gerade bewegtem Element der Fokus genommen

Beispiel
// Defokussieren bei ESC-Taste
function esc() {
	if(dragele) dragele.blur();
} // esc

Die Methode preventDefault() gilt nur für das aktuelle Event und muss daher in jedem Eventhandler aufgerufen werden. Die Einstellung touchAction = ... gilt dagegen dauerhaft.

Noch einige Abschlussarbeiten

Zum Schluss bekommen die bewegbaren Elemente noch das Attribut tabIndex = 1, um mit der Tastatur fokussierbar zu sein. Mit touchAction = 'none' bzw. e.preventDefault() wird bei diesen Elementen die Defaultaktion unterdrückt. Zuletzt wird noch der Cursor auf 'move' gesetzt und beim Fokussieren ein Rahmen um das Dragelement gelegt.

Beispiel
// Dragbares Element mit Tab-Index für die Fokussierbarkeit und Eventhandler für Unterdrückung der Standardaktion versehen
function finish(ele) {
	ele.tabIndex = 0;
	if(!pointer_event) {
		ele.addEventListener("touchmove", function(e) { e.preventDefault() }, false);
	}
} // finish

// finish für alle verschiebbaren Elemente aufrufen
const draggable = document.querySelectorAll("." + drag_class);
for(let i=0;i<draggable.length;i++) {
	finish(draggable[i]);
}

// css-Angaben für die Bedienbarkeit
const style = document.createElement('style');
style.innerText = "."+drag_class+":focus { outline: 2px solid blue; } "
                + "."+drag_class+" { position: absolute; cursor: move; touch-action: none; } ";
document.head.appendChild(style);

Mit Hilfe des MutationObserver wird die Funktion finish auch für nachträglich erzeugte bewegliche Elemente aufgerufen. Der MutationObserver überwacht alle Änderungen am DOM. Über den Ausdruck mutationsList[i].addedNodes[j].classList.contains(drag_class) wird bei allen Mutationen (mutationsList) bei allen hinzugefügten Knoten (addedNodes) geprüft, ob sie den entsprechenden Klassennamen haben (classList.contains):

Beispiel
// finish für nachträglich erzeugte verschiebbare Elemente aufrufen
new MutationObserver(function(mutationsList) {
	for(let i=0;i<mutationsList.length;i++) {
		if (mutationsList[i].type === 'childList') {
			for(let j=0;j<mutationsList[i].addedNodes.length;j++) {
				if(mutationsList[i].addedNodes[j].classList &&
				   mutationsList[i].addedNodes[j].classList.contains(drag_class)) {
					finish(mutationsList[i].addedNodes[j]);
				}
			}	
		}
	}
}).observe(document.body, { childList: true, subtree: true });

Das vollständige Drag and Drop Script

Das vollständige Script wird über den Eventhandler zum DOMContentLoaded-Event aufgerufen:

Beispiel ansehen …
// drag_n_drop.js
// 6. 1. 2021
// Alle Elemente mit der Klasse "draggable" werden verschiebbar gemacht

document.addEventListener("DOMContentLoaded", function() {

	"use strict"

	// Klasse für verschiebbare Elemente
	const drag_class = "draggable";
	
	// Prüfen, welche Eventmodelle unterstützt werden und welches verwendet werden soll
	const pointer_event = ("PointerEvent" in window);
	const touch_event = ("TouchEvent" in window) && !pointer_event;
	
	// Einige Variablen
	let pos0;           // Pointerposition bei down
	let start;          // Position des Dragobjekts bei down
	let zmax = 1000;    // Start z-Index für die Dragelemente, muss evtl. angepasst werden
	let dragele = null; // Das aktuelle Dragelement

	// Bestimmen der Pointerposition
	function get_pointer_pos(e) {
		let posx=0, posy=0;
		if(touch_event && e.targetTouches && e.targetTouches[0] && e.targetTouches[0].clientX) {
			posx = e.targetTouches[0].clientX;
			posy = e.targetTouches[0].clientY;
		}
		else if(e.clientX) {
			posx = e.clientX ;
			posy = e.clientY ;
		}
		return {x: posx, y: posy};
	} // get_pointer_pos
	
	//Eventhandler für pointerdown, touchstart oder mousedown
	function handle_down(e) { 
		const pos = get_pointer_pos(e);
		down(e,pos);
	} // handle_down

	//Eventhandler für pointermove, touchmove oder mousemove
	function handle_move(e) {
		const pos = get_pointer_pos(e);
		move(e,pos);
	} // handle_move

	//Eventhandler für pointerup, touchend oder mouseup
	function handle_up (e) {
		up(e);
	} // handle_up

	//Eventhandler für keydown
	function handle_keydown(e) {
		const keyCode = e.keyCode;
		let xwert = 0, ywert = 0;
		if(keyCode && (keyCode==27 || keyCode==37 || keyCode==38 || keyCode==39 || keyCode==40)) { 
			let delta = e.shiftKey?10:1;
			down(e,{x: 0,y: 0});
			switch(keyCode){
				case 37: // links
					xwert = -delta;
					break;
				case 38: // rauf
					ywert = -delta;
					break;
				case 39: // rechts
					xwert = delta;
					break;
				case 40: // runter
					ywert = delta;
					break;
				case 27: // Escape
					esc();
					up(e);
					return;
					break;
			}
			move(e,{x: xwert,y:ywert});
			up(e);
		}
	} // keydown

	// Auswahl des Dragelements und Start der Dragaktion
	function down(e,pos) {
		const target = parent(e.target,drag_class);
		if(target) {
			document.body.style.touchAction = "none";
			e.preventDefault();
			dragele = target;
			start = {x: dragele.offsetLeft, y: dragele.offsetTop};
			pos0 = pos;
			dragele.style.zIndex = ++zmax;
			dragele.focus();
		}
	} // down

	// Bewegen des Dragelements
	function move(e,pos) {
		if(dragele) {
			e.preventDefault();
			dragele.style.left = (start.x + pos.x - pos0.x) + "px";
			dragele.style.top =  (start.y + pos.y - pos0.y) + "px";
		}
	} // move

	// Ende der Aktion
	function up(e) { 
		if(dragele) {
			dragele = null;
			document.body.style.touchAction = "auto";
		}
	} // up

	// Defokussieren bei ESC-Taste
	function esc() {
		if(dragele) dragele.blur();
	} // esc

	// Dragbares Element mit Tab-Index für die Fokussierbarkeit und Eventhandler für Unterdrückung der Standardaktion versehen
	function finish(ele) {
		ele.tabIndex = 0;
		if(!pointer_event) {
			ele.addEventListener("touchmove", function(e) { e.preventDefault() }, false);
		}
	} // finish
	
	// Vorfahrenelement mit Klasse classname suchen
	function parent(child,classname) {
		if(child && "closest" in child) return child.closest("."+classname);
		let ele = child;
		while(ele) {
			if(ele.classList && ele.classList.contains(classname)) return ele;
			else ele = ele.parentElement;
		}
		return null;
	} // parent

	// Alle Eventhandler notieren
	if(pointer_event) {
		document.body.addEventListener("pointerdown",handle_down,false);
		document.body.addEventListener("pointermove",handle_move,false);
		document.body.addEventListener("pointerup",handle_up,false);
	}
	else if(touch_event) {
		document.body.addEventListener("touchstart",handle_down,false);
		document.body.addEventListener("touchmove",handle_move,false);
		document.body.addEventListener("touchend",handle_up,false);
	}
	else {
		document.body.addEventListener("mousedown",handle_down,false);
		document.body.addEventListener("mousemove",handle_move,false);
		document.body.addEventListener("mouseup",handle_up,false);
	}

	document.body.addEventListener("keydown",handle_keydown,false);

	// finish für alle verschiebbaren Elemente aufrufen
	const draggable = document.querySelectorAll("." + drag_class);
	for(let i=0;i<draggable.length;i++) {
		finish(draggable[i]);
	}

	// css-Angaben für die Bedienbarkeit
	const style = document.createElement('style');
	style.innerText = "."+drag_class+":focus { outline: 2px solid blue; } "
									+ "."+drag_class+" { position: absolute; cursor: move; touch-action: none; } ";
	document.head.appendChild(style);

	// finish für nachträglich erzeugte verschiebbare Elemente aufrufen
	new MutationObserver(function(mutationsList) {
		for(let i=0;i<mutationsList.length;i++) {
			if (mutationsList[i].type === 'childList') {
				for(let j=0;j<mutationsList[i].addedNodes.length;j++) {
					if(mutationsList[i].addedNodes[j].classList && mutationsList[i].addedNodes[j].classList.contains(drag_class)) {
						finish(mutationsList[i].addedNodes[j]);
					}
				}	
			}
		}
	}).observe(document.body, { childList: true, subtree: true });

},false); // DOMContentLoaded

Weblinks