JavaScript/Tutorials/Drag and Drop
Information
- 20min
- mittel
-
- JavaScript und CSS
- Positionieren eines Elements mit
style.top und style.left - Erstellen eines Stylesheets per Javascript
- Positionieren eines Elements mit
- JavaScript/DOM/Event
- JavaScript/Tutorials/Mouse and More
- Pointer-, Touch- und Mouse-Events
- JavaScript und CSS
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:
- Down-Phase: Das Element wird mit Maus (Taste drücken), Finger (Touch), o.Ä., also mit dem Pointer ausgewählt.
- Move-Phase: Durch Bewegen des Pointers wird das Element an eine andere Stelle gezogen.
- 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.
Inhaltsverzeichnis
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:
<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.
.draggable {
position: absolute;
}
Das Javascript
Ermitteln der Position eines Elements
Das Auslesen der Elementposition erfolgt über die Eigenschaften offsetLeft
und offsetTop
:
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.
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.
// 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:
//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:
// 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:
// 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:
// 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
// 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.
// 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
):
// 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:
// 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
- Ermitteln der Pointer- oder Maus-Position über clientX, clientY oder über targetTouches
- Überwachung der Änderungen im DOM mittels MutationObserver