Trying to use the HTML Drag and Drop API to simply allow a user to reposition a container element while dragging from a drag handle.
I have the element moving and repositioning using drag and dragend but it's looking a bit glitchy - the element snaps back to it's previous position before re-rendering in its dropped position.
I know there's a simple(r) solution using mousedown but hoping to use the draggable API as my app has global mousedown listeners for another use case.
It seems just super poor performance, but probably I'm doing something totally wrong with the API here!
Thanks so much!
let dragHandle = document.getElementById("drag-handle");
let container = document.getElementById("drag-container");
dragHandle.addEventListener("drag", (event) => {
container.style.left = event.screenX + "px";
container.style.top = event.screenY + "px";
});
dragHandle.addEventListener("dragend", (event) => {
container.style.left = event.clientX + "px";
container.style.top = event.clientY + "px";
});
.drag-handle {
cursor: grab;
}
.container {
position: absolute;
top: 5px;
left: 5px;
}
<div class="container" id="drag-container">
<div
class="drag-handle"
id="drag-handle"
draggable="true">drag me</div>
<div>
Related
Whenever the user starts dragging an element with draggable="true", the element has a translucent copy of the element you are dragging. Here is the example from W3Schools:
stop starting animation
Right side of the photo is important. That is the animation you get when you start dragging an element.
https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_ondrag
I have tried to use event.preventDefault(). However, the problem here is that this prevents onDrag from going at all while the element is moving and I need the data (mouse position and such) from onDrag.
It seems there are posts out there for how to stop the animation when you drop it but not start.
Just going off the example on the W3Schools site, I want the drag information without the ondragstart animation.
So if I modify the code by adding a preventDefault() on the ondragstart function:
function dragStart(event) {
console.log(event);
event.preventDefault(); //stops animation in right side of photo, but then won't let ondrag fire
event.dataTransfer.setData("Text", event.target.id);
}
function dragging(event) {
console.log(event);
document.getElementById("demo").innerHTML = "The p element is being dragged";
}
The animation you get when dragging goes away. However (as shown by my console.log) lines, the ondrag won't fire with this preventDefault on ondragstart. This is the information I need.
preventDefault inside ondrag doesn't stop that animation. Is this even possible?
SOLUTION
If anyone is wondering, I found what I needed to do in this case. You can set the image in javascript to a transparent one:
How to remove drag(Ghost) image?
so drag is still technically running but that ghostly image is gone. Wasn't thinking of right search terms.
In this case i would go for a different approach:
https://codepen.io/deibl31/pen/oNeXmPE?editors=1111
// HTML
<div id="myDiv">
</div>
// CSS
#myDiv {
position: absolute;
height: 100px;
width: 100px;
background: black;
color: white;
}
// JS
let myDiv = document.getElementById('myDiv');
let mouseDown = false;
let divPos = {x: 0, y: 0};
myDiv.addEventListener('mousedown', (event) => {
mouseDown = true;
});
window.addEventListener('mousemove', (event) => {
if (mouseDown) {
divPos.x = event.clientX;
divPos.y = event.clientY;
}
});
window.addEventListener('mouseup', (event) => {
if (mouseDown) {
myDiv.style.top = divPos.y + 'px';
myDiv.style.left = divPos.x + 'px';
}
mouseDown = false;
});
If anyone is wondering, I found what I needed to do in this case. You can set the image in JavaScript to a transparent one:
How to remove drag(Ghost) image?
so drag is still technically running but that ghostly image is gone. Wasn't thinking of right search terms.
I have just learned the basics of HTML5 draggable and droppable API. I do not know how to retain the dragged position at its dropped position. On dropping it changes its position.
Here is the basic HTML (relevant elements only):
onDragStart = function(ev) {
//...
}
drop_handler = function(ev) {
ev.preventDefault();
ev.target.appendChild(document.getElementById("id1"));
}
dragover_handler = function(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = "move";
}
<div id="id1" draggable="true" ondragstart="onDragStart(event)" style="border:2px solid green; cursor:pointer;width:100px;height:50px;">Dragged Div</div>
<div id="id2" style="position:absolute;left:100px;top:200px;border:2px solid red; cursor:pointer;width:200px;height:200px;" ondrop="drop_handler(event)" ondragover="dragover_handler(event)">Drop Div
</div>
I want the dragged element to remain at its final position where it it is dropped. Any pointers will be appreciated.
As mentioned in my comment, <div id="id1" ...> still has the default positioning which is static. Once you append it to the dropDiv, it assumes normal document flow behavior. Because it's a block element, it goes underneath the text that is already there and fills the width of the block.
If you want it to stay exactly where it is when you drop it, you need to give it position: absolute and take into account how the mouse moved while dragging it on the screen. In the dragStart event, we capture the original coordinates of the dragDiv and account for where the mouse was inside relative to the top-left corner. When we drop the dragDiv into the dropDiv, we set the absolute positioning and account for how much the mouse moved during the drag.
Since dragDiv is now a child of dropDiv, we need our new top and left values to be relative to dropDiv's coordinates rather than the entire screen, so we subtract out dropDiv's top and left values. Note well that some of these methods may not take into account the borders around the elements which may make it look like it's a pixel or two off -- to fix that you can either subtract a one or two pixels in the calculation or give them box-sizing: border-box.
let offsetX;
let offsetY;
onDragStart = function(ev) {
const rect = ev.target.getBoundingClientRect();
offsetX = ev.clientX - rect.x;
offsetY = ev.clientY - rect.y;
};
drop_handler = function(ev) {
ev.preventDefault();
const left = parseInt(id2.style.left);
const top = parseInt(id2.style.top);
id1.style.position = 'absolute';
id1.style.left = ev.clientX - left - offsetX + 'px';
id1.style.top = ev.clientY - top - offsetY + 'px';
id2.appendChild(document.getElementById("id1"));
};
dragover_handler = function(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = "move";
};
<div id="id1" draggable="true" ondragstart="onDragStart(event)" style="border:2px solid green; cursor:pointer;width:100px;height:50px;">Dragged Div</div>
<div id="id2" style="position:absolute;left:200px;top:50px;border:2px solid red; cursor:pointer;width:200px;height:200px;" ondrop="drop_handler(event)" ondragover="dragover_handler(event)">Drop Div
</div>
I'm stuck at finding a solution to my problem.
Let's assume we have fixed-sized area with scrollbars. We need to place an iframe inside so we can preview it by either using the scrollbars or by dragging it. To prevent iframe capturing mouse events, I've put absolute positioned transparent div above it.
<div style="" id="scrolling_container">
<div id="drag_div"></div>
<div id="frame_div">
<iframe id="page_iframe" src="http://www.bbc.com/" scrolling="auto" frameborder="0"></iframe>
</div>
</div>
Then I used some code that transforms mouse dragging into div scrolling.
var draggableContainer = document.getElementById("drag_div");
var scrollingContainer = document.getElementById("scrolling_container");
draggableContainer.removeEventListener('mousedown', draggableContainer.md, 0);
window.removeEventListener('mouseup', draggableContainer.mu, 0);
window.removeEventListener('mousemove', draggableContainer.mm, 0);
var pushed = 0;
draggableContainer.addEventListener('mousedown',
draggableContainer.md = function(e) {
pushed = 1;
lastClientX = e.clientX;
lastClientY = e.clientY;
e.preventDefault();
e.stopPropagation();
draggableContainer.style.cursor = "move";
}, 0
);
window.addEventListener('mouseup',
draggableContainer.mu = function() {
pushed = 0;
draggableContainer.style.cursor = "pointer";
}, 0
);
window.addEventListener('mousemove',
draggableContainer.mm = function(e) {
if (pushed) {
var offsetLeft = - lastClientX + (lastClientX=e.clientX),
offsetTop = - lastClientY + (lastClientY=e.clientY);
scrollingContainer.scrollLeft -= offsetLeft;
scrollingContainer.scrollTop -= offsetTop;
}
}, 0
);
Please take a look at the demo: jsFiddle
The problem is, overlay div is not stretched across the whole iframe, it's only spread to visible div area:
Screenshot
It works fine though if I delete this
#scrolling_container {
position: relative;
}
But I need it to be like that because otherwise overlay div overlaps scrollbars and I can no longer use them.
Is there any cross-browser solution that prevents iframe from capturing mouse events so I could get rid of overlay div?
You can disable any mouse interaction with the iframe if you give it the pointer-events:none; property in CSS.
#drag_div {
background:transparent;
position:relative;
width:100%;
height:480px; /* your iframe height */
top:480px; /* your iframe height */
margin-top:-480px; /* your iframe height */
}
probably check this link
Disable mouse scroll wheel zoom on embedded Google Maps
The Javascript onmouseup event is not triggered if the mouse button is released outside the element on which onmousedown has been triggered.
This causes a drag&drop bug in JQuery UI: A JQuery draggable element does not stop dragging when mouse button is released outside of its container (because the element will stop moving when reaching it's parent boundaries). Steps to reproduce:
Go to http://jqueryui.com/draggable/.
Drag the draggable downward until the mouse has left the surrounding container
Release mouse button (no mouse button is pressed at this point)
Move mouse back into container
And the draggable is still being dragged. I would expect the dragging to have stopped as soon as I released the mouse button - no matter where it is released.
I see that behavior in latest Chrome and IE.
Is there any work-around?
I know that we could stop dragging the container on mouseout or mouseleave, but I would like to keep dragging, even if I am outside the parent container, much like in google maps (no matter, where you release the mouse, it always stops dragging the map).
You can have your mousedown element "capture" the pointer. Then it would always receive the mouseup event. In React this could look like this:
const onPointerDownDiv1 = (event: React.PointerEvent) => {
(event.target as HTMLDivElement).setPointerCapture(event.pointerId);
// Do something ...
};
const onPointerUpDiv1 = (event: React.PointerEvent) => {
(event.target as HTMLDivElement).releasePointerCapture(event.pointerId);
// Do something ...
};
<div
ref={div1}
id="div1"
className="absolute top-[200px] left-[390px] h-8 w-8 bg-red-300"
onPointerDown={onPointerDownDiv1}
onPointerUp={onPointerUpDiv1}
/>
And here is an implementation using "plain vanilla" html + javascript:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div
id="div1"
style="
position: absolute;
left: 50px;
top: 50px;
width: 20px;
height: 20px;
background-color: red;
"
></div>
</body>
<script>
let isDragging = false;
let offsetX = 0;
let offsetY = 0;
let divElement = document.getElementById("div1");
divElement.addEventListener("pointerdown", onPointerDown);
divElement.addEventListener("pointermove", onPointerMove);
divElement.addEventListener("pointerup", onPointerUp);
function onPointerDown(event) {
divElement.setPointerCapture(event.pointerId);
offsetX = event.clientX - divElement.offsetLeft;
offsetY = event.clientY - divElement.offsetTop;
isDragging = true;
}
function onPointerMove(event) {
if (isDragging) {
divElement.style.left = (event.clientX - offsetX).toString() + "px";
divElement.style.top = (event.clientY - offsetY).toString() + "px";
}
}
function onPointerUp(event) {
divElement.releasePointerCapture(event.pointerId);
isDragging = false;
}
</script>
</html>
I found this to be the best solution: Attach the mouseup event handler to document instead. Then it will always cancel, even if you release the mouse button outside the browser. Of course, this is not a pretty solution, but apparently, this is the only way to get dragging to work correctly.
Try the solution below:
You will see that "Drag END" will always happen, no matter where you release the cursor.
Also, in order to prevent text selection while dragging, I added an unselectable class.
let dragging = false;
const dragEl = document.querySelector('div');
const logEl = document.querySelector('pre');
dragEl.addEventListener('mousedown touchstart', (evt) => {
dragging = true;
dragEl.classList.add('unselectable');
logEl.textContent += 'drag START\n';
});
document.addEventListener('mouseup touchend', (evt) => {
if (dragging) {
event.preventDefault();
dragEl.classList.remove('unselectable');
dragging = false;
logEl.textContent += 'drag END\n';
}
});
div {
background: red;
}
.unselectable {
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard */
}
<div>drag me</div>
<hr>
LOG:
<p><pre></pre></p>
Update
These days, the setPointerCapture API provides a cleaner solution, as explained in this answer.
The best way to handle this is to check the current button status in the mousemove event handler. If the button is no longer down, stop dragging.
If you put the mouseup event on the document then you just move the problem up, the original problem will still show itself if you release the button outside of the document (or even outside of the browser window).
For example with jQuery
$div.on("mousedown", function (evt) {
dragging = true;
}).on("mouseup", function (evt) {
dragging = false;
}).on("mousemove", function (evt) {
if (dragging && evt.button == 0) {
//Catch case where button released outside of div
dragging = false;
}
if (dragging) {
//handle dragging here
}
});
You could skip the mouseup event handler because it will be caught by the mousemove but I think it's more readable with it still there.
If you create an "index.html" file with the following code in it (you can just copy and paste it entirely) it will show the working solution using just plain vanilla html + javascript.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div
id="div1"
style="
position: absolute;
left: 50px;
top: 50px;
width: 20px;
height: 20px;
background-color: red;
"
></div>
</body>
<script>
let isDragging = false;
let offsetX = 0;
let offsetY = 0;
let divElement = document.getElementById("div1");
divElement.addEventListener("pointerdown", onPointerDown);
divElement.addEventListener("pointermove", onPointerMove);
divElement.addEventListener("pointerup", onPointerUp);
function onPointerDown(event) {
divElement.setPointerCapture(event.pointerId);
offsetX = event.clientX - divElement.offsetLeft;
offsetY = event.clientY - divElement.offsetTop;
isDragging = true;
}
function onPointerMove(event) {
if (isDragging) {
let newPosLeft = event.clientX - offsetX;
if (newPosLeft < 30) {
newPosLeft = 30;
}
let newPosTop = event.clientY - offsetY;
if (newPosTop < 30) {
newPosTop = 30;
}
divElement.style.left = newPosLeft.toString() + "px";
divElement.style.top = newPosTop.toString() + "px";
}
}
function onPointerUp(event) {
divElement.releasePointerCapture(event.pointerId);
isDragging = false;
}
</script>
</html>
I have two columns in my HTML page.
<div id="content">
<div id="left"></div>
<div id="right"></div>
</div>
Each of them occupies half of the page
#content {
height: 100%;
}
#left, #right {
float: left;
width: 50%;
height: 100%;
overflow: auto;
}
I'd like the boundary between left and right halves to be adjustable by the user. That is, the user can move the boundary to the left or to the right as he/she browses the page. Is it possible to do that somehow?
Yes, but it requires JavaScript. To apply it, you could of course just set the width of each of the sides:
var leftPercent = 50;
function updateDivision() {
document.getElementById('left').style.width = leftPercent + '%';
document.getElementById('right').style.width = (100 - leftPercent) + '%';
}
Now you can adjust the division with, say leftPercent = 50; updateDivision(), but the user isn't going to do that. There are multiple different ways you could present this to the user. Probably the best-suited way would be a little line in the middle they could drag. For this, you could use a little CSS for the positioning:
#content {
position: relative;
}
#divider {
position: absolute;
/* left to be set by JavaScript */
width: 1px;
top: 0;
bottom: 0;
background: black;
cursor: col-resize;
/* feel free to customize this, of course */
}
And then make sure you've got a div with an id of divider in content and update updateDivision to also update the left of divider:
document.getElementById('left').style.left = leftPercent + '%';
Then you just need a little logic to handle the dragging. (Here, I've put all of the elements into appropriately-named variables):
divider.addEventListener('mousedown', function(e) {
e.preventDefault();
var lastX = e.pageX;
document.documentElement.addEventListener('mousemove', moveHandler, true);
document.documentElement.addEventListener('mouseup', upHandler, true);
function moveHandler(e) {
e.preventDefault();
e.stopPropagation();
var deltaX = e.pageX - lastX;
lastX = e.pageX;
leftPercent += deltaX / parseFloat(document.defaultView.getComputedStyle(content).width) * 100;
updateDivision();
}
function upHandler(e) {
e.preventDefault();
e.stopPropagation();
document.documentElement.removeEventListener('mousemove', moveHandler, true);
document.documentElement.removeEventListener('mouseup', upHandler, true);
}
}, false);
You should be able to read it to see how it works, but in short: It listens for when someone presses on the divider. When they do, it'll attach listeners to the page for when they move their mouse. When they do, it updates the variable and calls updateDivision to update the styles. When eventually it gets a mouseup, it stops listening on the page.
As a further improvement, you could make every element have an appropriate cursor style while dragging so your cursor doesn't flash while dragging it.
Try it out.
There's nothing in the divisions so nothing will happen. It's like writing:
<h1></h1>
And changing the CSS for h1 and expecting something to be there