I have a draggable element. After dragging, I want to add the click-funtion again. The click-event should not fire when dragging, but I want to add it when dragging is over.
If I add my click-event again after dragging (line 46) it get's fired immediately
clickElemtent(document.getElementById("mydiv"));
I don't understand the logic.. Thank you so much!
dragElement(document.getElementById("mydiv"));
clickElemtent(document.getElementById("mydiv"));
function clickElemtent(elmnt) {
elmnt.onclick = function() {
alert("click")
}
}
function dragElement(elmnt) {
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (document.getElementById(elmnt.id + "header")) {
document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
} else {
elmnt.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
elmnt.onclick = null;
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
}
function closeDragElement(e) {
document.onmouseup = null;
document.onmousemove = null;
e.stopImmediatePropagation();
e.stopPropagation();
clickElemtent(document.getElementById("mydiv")); ///// ?????
}
}
#mydiv {
position: absolute;
z-index: 9;
background-color: #f1f1f1;
text-align: center;
border: 1px solid #d3d3d3;
}
#mydivheader {
padding: 10px;
cursor: move;
z-index: 10;
background-color: #2196F3;
color: #fff;
}
<div id="mydiv">
<div id="mydivheader">click OR drag</div>
</div>
mouseup
and click
are different events (even though both are caused by the same interaction!), so stopping one doesn't affect the other.
(An explanation to Bujaq's answer:)
An event's propagation path is determined before propagation. That means adding/removing click
listeners in another click
listener won't have any effect on the current event's path.
However, different event types can affect each other, even those from the same event source, e.g. a mouse click. Events are run in a fixed order; for mouse events, mouseup
runs before click
.
Taking the event loop into consideration: Calls to the event handlers are queued, so tasks added via setTimeout()
will be run later. That is also the reason why 0 ms is enough, too.
That means, adding the onclick
like that will only have an effect after the pending events have been handled.
You want to only accept click
events if no dragging occured. That means we need a way to tell if dragging occured.
Dragging has always occured if a mousemove
event happened after a mousedown
event.
Since a click
event is fired even when dragging, we can call the "actual" listener only when no dragging occured:
var clickHandler = function(e) {
alert("click");
};
dragElement(document.getElementById("mydiv"), clickHandler);
function dragElement(elmnt, clickCallback /*New*/) {
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
var isDragged = false; // New
if (document.getElementById(elmnt.id + "header")) {
document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
} else {
elmnt.onmousedown = dragMouseDown;
}
if (clickCallback) {
elmnt.onclick = function(e) {
// Only call if no dragging occured
if (!isDragged) clickCallback(e);
};
}
function dragMouseDown(e) {
isDragged = false; // No dragging since now
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
isDragged = true; // Dragging occured
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
}
function closeDragElement(e) {
document.onmouseup = null;
document.onmousemove = null;
}
}
#mydiv {
position: absolute;
z-index: 9;
background-color: #f1f1f1;
text-align: center;
border: 1px solid #d3d3d3;
}
#mydivheader {
padding: 10px;
cursor: move;
z-index: 10;
background-color: #2196F3;
color: #fff;
}
<div id="mydiv">
<div id="mydivheader">click OR drag</div>
</div>
Alternatively to conditionally calling the click
listener, we can just stop the event from propagating.
We need to stop the event before it reaches the expected listener. This can be done in two ways:
Both ways require the use of addEventListener()
to attach the listeners, which is the preferred way. The onevent
properties and especially inline event attributes are generally discouraged.
In the end we want to make use of the event flow. Example:
const select = document.querySelector("select");
const button = document.querySelector("button");
// Notice order of attachment
button.addEventListener("click", function stopEarly(evt) {
if (select.value === "early") evt.stopImmediatePropagation();
});
button.addEventListener("click", () => console.log("Button reached!"));
button.addEventListener("click", function stopInCapture(evt) {
if (select.value === "capture") evt.stopPropagation();
}, { capture: true });
button.addEventListener("click", function stopEarly(evt) {
if (select.value === "late") evt.stopImmediatePropagation();
}, { capture: false /*default*/ });
<select>
<option value=none>No stopping</option>
<option value=capture>Stop in capture</option>
<option value=early>Stop early</option>
<option value=late>Stop late</option>
</select>
<button>Log text</button>
If we ensure that the propagation-stopping listener is called before the "intended" listener, we don't have to remove and reattach any listeners at all:
var clickHandler = function(e) {
alert("click");
};
dragElement(document.getElementById("mydiv"), clickHandler);
function dragElement(elmnt, clickCallback /*New*/) {
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
var isDragged = false; // New
if (document.getElementById(elmnt.id + "header")) {
document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
} else {
elmnt.onmousedown = dragMouseDown;
}
if (clickCallback) {
elmnt.addEventListener("click", function(e) {
// Stop here if dragging occured
if (isDragged) e.stopImmediatePropagation();
}, { capture: true });
elmnt.addEventListener("click", clickCallback);
}
function dragMouseDown(e) {
isDragged = false; // No dragging since now
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
isDragged = true; // Dragging occured
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
}
function closeDragElement(e) {
document.onmouseup = null;
document.onmousemove = null;
}
}
#mydiv {
position: absolute;
z-index: 9;
background-color: #f1f1f1;
text-align: center;
border: 1px solid #d3d3d3;
}
#mydivheader {
padding: 10px;
cursor: move;
z-index: 10;
background-color: #2196F3;
color: #fff;
}
<div id="mydiv">
<div id="mydivheader">click OR drag</div>
</div>
Apart from the three sections above, I want to demonstrate one more thing:
As seen in the event flow, an event goes through multiple phases:
Events propagate down to and up from their target. That means we can use a single listener on an ancestor for multiple targets. This is called event delegation.
Assuming that only one element can be dragged at once, we can attach a single listener on an ancestor, where we'll stop propagation when applicable.
Under these conditions, we can make elements automatically behave as wished simply by adding a class:
const button = document.querySelector("button");
// Add *click* listener to "click or drag" button
button.addEventListener("click", () => {
console.log("Initial button");
});
// Adding new draggable buttons is simple:
const button2 = document.createElement("button");
button2.classList.add("draggable"); // For dragging functionality
button2.classList.add("blue-button"); // For styling
button2.addEventListener("click", () => console.log("Button 2!")); // The *click* listener
button2.textContent = "Click or Drag 2";
document.body.append(button2);
(function pageWideDraggable() {
let target = null;
let isDragged = false;
document.addEventListener("mousedown", evt => {
// Get draggable target and reset "dragging memory"
target = event.target.closest(".draggable");
isDragged = false;
});
document.addEventListener("mousemove", evt => {
// If draggable target exists, remember that dragging occured and update target's position
if (!target) return;
isDragged = true;
target.style.top = `${target.offsetTop + evt.movementY}px`;
target.style.left = `${target.offsetLeft + evt.movementX}px`;
});
document.addEventListener("click", evt => {
// Capture click and stop event when dragging occured
if (isDragged) evt.stopPropagation();
target = null;
}, { capture: true });
})();
.draggable {
position: absolute;
padding: 0;
cursor: pointer;
user-select: none;
}
.blue-button {
padding: 10px;
display: inline-block;
color: white;
background-color: #2196F3;
}
<button class="draggable blue-button">
click OR drag
</button>
Sidenote: Notice how the deeper elements don't need to be aware of the capturing. This allows for easy component-based development.