I wrote this code to make any element with class draggable draggable.
const d = document.getElementsByClassName("draggable");
for (let i = 0; i < d.length; i++) {
d[i].style.position = "relative";
}
function filter(e) {
let target = e.target;
if (!target.classList.contains("draggable")) {
return;
}
target.moving = true;
e.clientX ?
(target.oldX = e.clientX,
target.oldY = e.clientY) :
(target.oldX = e.touches[0].clientX,
target.oldY = e.touches[0].clientY)
target.oldLeft = window.getComputedStyle(target).getPropertyValue('left').split('px')[0] * 1;
target.oldTop = window.getComputedStyle(target).getPropertyValue('top').split('px')[0] * 1;
document.onmousemove = dr;
document.addEventListener('touchmove', dr, {passive: false})
function dr(event) {
event.preventDefault();
if (!target.moving) {
return;
}
event.clientX ?
(target.distX = event.clientX - target.oldX,
target.distY = event.clientY - target.oldY) :
(target.distX = event.touches[0].clientX - target.oldX,
target.distY = event.touches[0].clientY - target.oldY)
target.style.left = target.oldLeft + target.distX + "px";
target.style.top = target.oldTop + target.distY + "px";
}
function endDrag() {
target.moving = false;
}
target.onmouseup = endDrag;
target.ontouchend = endDrag;
}
document.onmousedown = filter;
document.ontouchstart = filter;
div {
width: 100px;
height: 100px;
background: red;
}
<div class="draggable"></div>
tl;dr- I want to make a Windows taskbar thing where elements can be moved and the other elements move to the right or left based on where the dragged element is approaching it.
I want the draggable elements to snap to the grid similar to what happens when you drag an icon on the Windows taskbar or a tab on another tab in your browser.
Following is my attempt. I removed movement along the verticle axis and touch support to make the code more readable. The snapping is working fine but the element being hovered is not moving to the other space.
const d = document.getElementsByClassName("draggable");
let grid = 50;
for (let i = 0; i < d.length; i++) {
d[i].style.position = "relative";
d[i].onmousedown = filter;
}
function filter(e) {
let target = e.target;
target.moving = true;
target.oldX = e.clientX;
target.oldLeft = window.getComputedStyle(target).getPropertyValue('left').split('px')[0] * 1;
document.onmousemove = dr;
function dr(event) {
event.preventDefault();
if (!target.moving) {
return;
}
target.distX = event.clientX - target.oldX;
target.style.left = target.oldLeft + Math.round(target.distX / grid) * grid + 'px'
}
function endDrag() {
target.moving = false;
}
document.onmouseup = endDrag;
}
.parent {
width: 100%;
height: 100%;
background: lime;
display: flex;
align-items: center;
}
.child {
width: 50px;
height: 50px;
position: relative;
}
.one {
background: red;
}
.two {
background: blue;
}
<div class="parent">
<div class="child one draggable"></div>
<div class="child two draggable"></div>
</div>
Further, I think checking for when the mouse has crossed half of the width of a div, while an element is being dragged, the div should move either one unit left or right depending whether the element is left or right to the element being dragged. The checking part is no trouble. We can just compare the magnitudes of the elements' offsetLeft. But how do I make the element move?
Please try to answer in vanilla javascript.
Edits: 1. Updated code 2. Updated title 3. Updated tl;dr and changed title 3.Added more tags
Method 1: Pure JS Code continuing from question
The following code fulfills the question's demand. I added more elements to the original code (more elements = more fun). I have also added comments in the code. I am sure you will understand the code just by reading it. Here is a brief explanation anyway.
Snapping target in a grid
We first need to snap elements in a grid. We first snap target, see the next section for snapping other elements. The grid's width in pixels is specified in the line let grid = .... For smooth animation, we want the target to snap when we end dragging, not while we are dragging. This line of code in the function endDrag snaps the target into grid when drag is over.
target.style.left = target.oldLeft + Math.round(target.distX / grid) * grid + "px";
Moving other elements based on target
We also need to move the element whose position target takes. Otherwise, they would overlap. The function moveElementAt does this job. This is what happens in moveElementAt.
We name any element that collides with the target's top-left corner elementAt. The JavaScript property .elementFromPoint does the check.
In the checking, we exclude the target itself by setting its CSS pointer-events to none. We do nothing if elementAt is the parent element.
We check if the element is approaching at elementAt from left or right by some mathematical logic.
If the target is coming from the right, elementAt moves grid units towards the right.
If the target is coming from left elementAt moves grid units left.
const d = document.getElementsByClassName("draggable");
let grid = 50; //Width of one grid box
for (let i = 0; i < d.length; i++) {
d[i].style.position = "relative";
}
function filter(e) {
let target = e.target;
target.moving = true;
target.oldX = e.clientX;
target.oldLeft =
window
.getComputedStyle(target)
.getPropertyValue("left")
.split("px")[0] * 1; //Get left style as a number
document.onmousemove = dr;
function dr(event) {
event.preventDefault();
if (!target.moving) {
return;
}
target.distX = event.clientX - target.oldX;
target.style.left = target.oldLeft + target.distX + "px";
target.style.pointerEvents = "none"; //Stops target from being elementAt
moveElementAt();
}
function endDrag() {
target.moving = false;
target.style.left =
target.oldLeft + Math.round(target.distX / grid) * grid + "px";
moveElementAt(); //Do it at endDrag() also to stop elements from overlapping
target.style.pointerEvents = "auto";
}
function moveElementAt() {
let rootEl = target.parentNode;
let elementAt = document.elementFromPoint(
target.offsetLeft,
target.offsetTop //Get element at target's coordinates
);
if (elementAt === rootEl) {
return
} //Stop rootEl from moving
//Move elementAt either grid units left or right depending on which way target is approaching it from
if (target.offsetLeft - elementAt.offsetLeft * 1 <= grid / 2) //Can also compare to 0, comparing to grid/2 stops elements' position from breaking when moving very fast to some extent
{
elementAt.style.left =
window
.getComputedStyle(elementAt)
.getPropertyValue("left")
.split("px")[0] * 1 - grid + "px";
} else {
elementAt.style.left =
window
.getComputedStyle(elementAt)
.getPropertyValue("left")
.split("px")[0] * 1 + grid + "px";
}
}
document.onmouseup = endDrag;
}
document.onmousedown = filter;
.parent {
width: 100%;
height: 100%;
background: lime;
display: flex;
align-items: center;
}
.child {
width: 50px;
height: 50px;
position: relative;
}
.one {
background: red;
}
.two {
background: blue;
}
.three {
background: brown;
}
.four {
background: pink;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hello!</title>
<link rel="stylesheet" href="/style.css" />
<script src="/scriptmain.js"></script>
</head>
<body>
<div class="parent" id="parent">
<div class="child one draggable"></div>
<div class="child two draggable"></div>
<div class="child three draggable"></div>
<div class="child four draggable"></div>
</div>
</body>
</html>
The code is a bit glitchy when dragging unusually fast. I will fix the glitch later though.
Method 2: Using the Drag and Drop API / a library
Jon Nezbit notified me that there is a library called SortableJs specifically meant for this purpose. The question stated for a pure JS solution. So I coded a method that used the drag and drop API. Here is a snippet.
function sortable(rootEl) {
let dragEl;
for (let i = 0; i < rootEl.children.length; i++) {
rootEl.children[i].draggable = true;
}
rootEl.ondragstart = evt => {
dragEl = evt.target;
rootEl.addEventListener("dragover", onDragOver);
rootEl.addEventListener("dragend", onDragEnd);
};
function onDragOver(evt) {
let target = evt.target;
rootEl.insertBefore(
dragEl,
rootEl.children[0] === target ?
rootEl.children[0] :
target.nextSibling || target
);
}
function onDragEnd(evt) {
evt.preventDefault();
rootEl.removeEventListener("dragover", onDragOver);
rootEl.removeEventListener("dragend", onDragEnd);
}
}
sortable(document.getElementById("parent"))
.parent {
width: 100%;
height: 100%;
background: lime;
display: flex;
align-items: center;
}
.child {
width: 50px;
height: 50px;
position: relative;
}
.one {
background: red;
}
.two {
background: blue;
}
.three {
background: brown;
}
.four {
background: pink;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hello!</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<div class="parent" id="parent">
<div class="child one draggable"></div>
<div class="child two draggable"></div>
<div class="child three draggable"></div>
<div class="child four draggable"></div>
</div>
<script src="/script-dndmain.js"></script>
</body>
</html>
The library uses the HTML Drag and Drop API which does not give me the result as I wanted. But you should definitely check that out. Also, check out this excellent article from the author of the library which explains (with pure js) how they made that library. Although I did not use it, I am sure someone will be helped out.
Related
This question already has an answer here:
scroll events: requestAnimationFrame VS requestIdleCallback VS passive event listeners
(1 answer)
Closed 8 months ago.
Sorry if this is a dumb question, I am super new to programming in html and Javascript.
I know, that I can change some transformation with the scrollpositon, but right now I'm a bit lost. I want to increase the height of a box, when the user scrolls down.
It starts with
"height: 60px;"
and then I'd like it to grow like "height ="60 + scrollY * a very small number + px".
Is there any way to achieve this?
Kind regards and greetings!
let height = 60;//default height
document.addEventListener("mousewheel", function(event){
if(event.wheelDelta >= 0){
height--
element.style.height = `${height}px`
}else{
height++
element.style.height = `${height}px`
}
})
You can use the scroll_event to enlarge the box when a user scrolls.
Here is an example of how to change the box height on scroll using dynamic calculation.
<html lang="en">
<head>
<title>Scroll large demo</title>
<style>
body {
min-height: 1000px;
background: aliceblue;
}
#box {
height: 60px;
background: red;
color: white;
vertical-align: middle;
text-align: center;
position: fixed;
margin: 0 auto;
padding: 15px;
}
</style>
</head>
<body>
<div id="box">
I am going to enlarge when you scroll
</div>
<script>
let lastKnownScrollPosition = 0;
let ticking = false;
function doSomething(scrollPos) {
document.getElementById("box").style.height = 60 + (scrollPos * 0.25) + "px";
}
document.addEventListener('scroll', function (e) {
lastKnownScrollPosition = window.scrollY;
doSomething(lastKnownScrollPosition);
if (!ticking) {
window.requestAnimationFrame(function () {
doSomething(lastKnownScrollPosition);
ticking = false;
});
ticking = true;
}
});
</script>
</body>
</html>
Here is my JSFiddle: https://jsfiddle.net/y6pnq7k5/3/
Sorry if it's too much code, I tried to narrow it down.
The main functions to look at are generatorClicked() and progressBarAnimation().
When you click on the boxes (for example fire spell tome), I have a progress bar animation that goes across the div. The problem is that if you spam click the div it will reset the progress bar back at the start, and because this is meant to be an idle game the progress bar should not be affected until it has finished it's job.
One solution I've tried are disabling pointer events via style:
var isClicked = true;
document.GetElementByID(this.id).style.pointerEvents = 'none';
//progress bar animation - after it's done isClicked = false
if(!isClicked) {
document.GetElementByID(this.id).style.pointerEvents = 'auto';
}
This had issues with either the progress bar not being clickable after the first progress bar finished, or it would still allow the click spam to reset the bar, depending on where I made isClicked false.
I've also tried creating a shield like the top answer in this post
Which just stopped the progress bar from moving at all.
I don't have any libraries, just vanilla JS. I know that my implementation of this progress bar animation is also dubious, but even if I refactor it I still would need the progress bar to not be affected by subsequent clicks as it will affect the gameplay.
pointer-events CSS property can be set to 'none' before the progress bar animation starts and reset to its original value after the progress bar animation stops.
var fireGenerators = ["fireSpellTome", "fireWizard", "fireTeacher", "fireSchool"];
var fireGeneratorStrings = ["Fire Spell Tome", "Fire Wizard", "Fire Teacher", "Fire School"];
var fireGeneratorAmount = new Array();
function initFireGenerators() {
for (i = 0; i < fireGenerators.length; i++) {
fireGeneratorAmount.push(0);
var div = document.createElement('div');
div.id = fireGenerators[i];
div.className = "fireGenerators";
div.innerHTML = fireGeneratorStrings[i];
div.onclick = generatorClicked;
var progressBar = document.createElement('div');
progressBar.id = fireGenerators[i] + "ProgressBar";
progressBar.className = "progressBar";
var amount = document.createElement('span');
amount.id = "amountFireGen" + i;
amount.class = "fireAmounts";
amount.innerHTML = "0";
div.appendChild(progressBar);
div.appendChild(amount);
main.appendChild(div);
}
}
function generatorClicked() {
console.log(this.id + " clicked");
progressBarAnimation(this.id);
}
function progressBarAnimation(parentID) {
const progressBarContainer = document.getElementById(parentID);
const progressBar = document.getElementById(parentID + "ProgressBar"); //Get progressBar div element
let id = null;
let currentWidth = 0; //Set the width to 0
clearInterval(id);
progressBarContainer.style.pointerEvents = "none";
id = setInterval(frame, 10); //TEMPORARY - Need to check what generator and match progress with speed
function frame() {
if (currentWidth == 330) {
clearInterval(id);
progressBar.style.width = "0px";
progressBarContainer.style.pointerEvents = "auto";
} else {
currentWidth++;
progressBar.style.width = currentWidth + "px";
}
}
return true;
}
initFireGenerators();
html, body {
height: 100%;
width: 100%;
margin: 0;
font-family: Arial, Helvetica, sans-serif;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-ms-user-select: none;
}
div span {
margin-left: 15px;
}
#main {
width: 100%;
height: 80%;
}
#fireSpellTome {
background-size: 100% 100%;
background-image: url('../images/firetome.png');
background-repeat: no-repeat;
}
.fireGenerators {
position: relative;
width: 330px;
height: 170px;
border:2px solid #000;
margin-top: 1px;
}
.progressBar {
position: absolute;
top: 0;
width: 0;
height: 100%;
background-color: #111;
opacity: 0.5;
}
.noPointers {
pointer-events: none;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="stylesheets/game.css">
</head>
<body>
<h1><span id="fireMagicAmount">0</span><br></h1>
<div id=main></div>
</body>
</html>
I took a look at this article and I wanted to try this same technique with more divs.
The above code works with 2 divs but not 4 divs. I tried to figure out why so I decided to try the following code.
var handler = document.querySelector('.handler');
var wrapperWidth;
var wrapper = handler.closest('.wrapper');
var box = wrapper.querySelector('.box');
var isHandlerDragging = false;
document.addEventListener('mousedown', function(e) {
// If mousedown event is fired from .handler, toggle flag to true
if (e.target === handler) {
isHandlerDragging = true;
}
});
document.addEventListener('mousemove', function(e) {
// Don't do anything if dragging flag is false
if (!isHandlerDragging) {
return false;
}
// Get offset
var containerOffsetLeft = wrapper.offsetLeft;
// Get x-coordinate of pointer relative to container
var pointerRelativeXpos = e.clientX - containerOffsetLeft;
// Arbitrary minimum width set on box A, otherwise its inner content will collapse to width of 0
var boxAminWidth = 60;
// Resize box A
// * 8px is the left/right spacing between .handler and its inner pseudo-element
// * Set flex-grow to 0 to prevent it from growing
wrapperWidth = wrapper.stlye.width;
box.style.width = (Math.max(boxAminWidth, wrapperWidth - 8)) + 'px';
box.style.flexGrow = 0;
});
document.addEventListener('mouseup', function(e) {
// Turn off dragging flag when user mouse is up
isHandlerDragging = false;
});
.wrapper {
background-color: #fff;
color: #444;
/* Use flexbox */
display: flex;
}
.box {
background-color: #444;
color: #fff;
border-radius: 5px;
padding: 20px;
font-size: 150%;
/* Use box-sizing so that element's outerwidth will match width property */
box-sizing: border-box;
/* Allow box to grow and shrink, and ensure they are all equally sized */
flex: 1 1 1 1 auto;
}
.handler {
width: 20px;
padding: 0;
cursor: ew-resize;
flex: 0 0 auto;
}
.handler::before {
content: '';
display: block;
width: 4px;
height: 100%;
background: red;
margin: 0 auto;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div class="wrapper">
<div class="box">A</div>
<div class="handler"></div>
<div class="box">B</div>
<div class="handler"></div>
<div class="box">C</div>
<div class="handler"></div>
<div class="box">D</div>
</div>
</body>
</html>
What I wanted to have happen was the divs to be arrangeable.
You can take a look at the code here.
https://jsfiddle.net/paralaxwombat/1Lfqdb6x/
If this is what you want
var wrapper = document.querySelector('.wrapper');
var box = null;
var isHandlerDragging = false;
var boxAminWidth = 60;
var new_width = 0, current_width = 0;
document.addEventListener('mousedown', function(e) {
// If mousedown event is fired from .handler, toggle flag to true
if (e.target.classList.contains('handler')) {
isHandlerDragging = true;
box = e.target.previousElementSibling;
}
});
document.addEventListener('mousemove', function(e) {
// Don't do anything if dragging flag is false or
if (!isHandlerDragging) {
return false;
}
// save the current box width
current_width = box.style.width;
// check the minimum width
if ((new_width = e.clientX - box.offsetLeft - 8 ) >= boxAminWidth) {
box.style.width = new_width + 'px';
}
// make sure the boxs dont go past the wrapper, aka: the overflow effect
//if they do, we recover the last width of the current box to keep the boxs inside the wrapper.
if(wrapper.lastElementChild.offsetLeft + wrapper.lastElementChild.offsetWidth > wrapper.offsetWidth) {
box.style.width = current_width;
}
});
document.addEventListener('mouseup', function(e) {
// Turn off dragging flag when user mouse is up
isHandlerDragging = false;
});
.wrapper {
background-color: #fff;
color: #444;
/* Use flexbox */
display: flex;
}
.box {
background-color: #444;
color: #fff;
border-radius: 5px;
padding: 20px;
font-size: 150%;
/* Use box-sizing so that element's outerwidth will match width property */
box-sizing: border-box;
/* Allow box to grow and shrink, and ensure they are all equally sized */
flex: 1 1 1 1 auto;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
flex-grow: 0;
flex-shrink: 0;
}
.handler {
width: 20px;
padding: 0;
cursor: ew-resize;
flex: 0 0 auto;
}
.handler::before {
content: '';
display: block;
width: 4px;
height: 100%;
background: red;
margin: 0 auto;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div class="wrapper">
<div class="box">A</div>
<div class="handler"></div>
<div class="box">B</div>
<div class="handler"></div>
<div class="box">C</div>
<div class="handler"></div>
<div class="box">D</div>
</div>
</body>
</html>
I have some observations:
var handler = document.querySelector('.handler');
this line of code (unlike jQuery) selects only the first handler, not all of them, so this check if (e.target === handler) is valid only for the first handler, thus the mousemove won't work on all of them.
same thing goes for var box = wrapper.querySelector('.box');, you'll be always setting with to the first box.
This is the new javaScript code
var wrapper = document.querySelector('.wrapper');
var box = null;
var isHandlerDragging = false;
var boxAminWidth = 60;
var new_width = 0, current_width = 0;
document.addEventListener('mousedown', function(e) {
// If mousedown event is fired from .handler, toggle flag to true
if (e.target.classList.contains('handler')) {
isHandlerDragging = true;
box = e.target.previousElementSibling;
}
});
document.addEventListener('mousemove', function(e) {
// Don't do anything if dragging flag is false or
if (!isHandlerDragging) {
return false;
}
// save the current box width
current_width = box.style.width;
// check the minimum width
if ((new_width = e.clientX - box.offsetLeft - 8 ) >= boxAminWidth) {
box.style.width = new_width + 'px';
}
// make sure the boxs dont go past the wrapper, aka: the overflow effect
//if they do, we recover the last width of the current box to keep the boxs inside the wrapper.
if(wrapper.lastElementChild.offsetLeft + wrapper.lastElementChild.offsetWidth > wrapper.offsetWidth) {
box.style.width = current_width;
}
});
document.addEventListener('mouseup', function(e) {
// Turn off dragging flag when user mouse is up
isHandlerDragging = false;
});
In CSS, I made a small change in the box class:
.box {
/* ... */
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
flex-grow: 0;
flex-shrink: 0;
}
I want to implement a vertical resizable div such that it increases height in certain fixed intervals. I don't want to use any libraries and looking for plain JS implementation. Here is jquery version: https://jqueryui.com/resizable/#snap-to-grid
This code works good but increases height by one pixel on each mousemove (vertically).
When I try to increase height by 10, the height increases but the mouse pointer does not stick to the base of div..,
https://plnkr.co/edit/3VesixHE2B7N46f8N7Bg?p=preview
const divElement = document.querySelector('.ns-resize');
const element = document.querySelector('.resizable');
let original_height = parseFloat(getComputedStyle(element, null).getPropertyValue('height').replace('px', ''));
let original_Y = element.getBoundingClientRect().top;
divElement.onmousedown = function(mouseDownEvent) {
mouseDownEvent.preventDefault();
var original_mouse_y = mouseDownEvent.pageY;
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stopResize);
function resize(mouseUpEvent) {
//cmp.increment = cmp.increment + 15;
const height = original_height + (mouseUpEvent.pageY - original_mouse_y);
if (height > 15 && height < 900) {
mouseUpEvent.pageY = mouseUpEvent.pageY ;
element.style.height = height + 'px';
divElement.innerHTML = element.style.height;
}
}
function stopResize() {
document.removeEventListener('mousemove', resize)
}
}
/* Styles go here */
.resizable {
width: 100px;
height : 100px;
border:1px solid red;
position: absolute;
}
.container {
position : relative;
}
.ns-resize {
position: absolute;
bottom: 0px;
cursor: ns-resize;
width: 100%;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="resizable">
<div class="ns-resize">
<span> </span>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
}
I could increase height by one pixel on every mousemove event, but what I really want is to increase by 10px each time.
As indicated in my comment, CSS already has a resize property you can use to make an element resizable. The only thing you need to do after a resize, is round the current height to the nearest 10px.
document.querySelector( '.resizable' ).addEventListener( 'mouseup', event => {
const current_height = parseInt( event.target.style.height, 10 );
event.target.style.height = Math.round( current_height / 10 ) * 10 + 'px';
console.log( `changed height from ${ current_height }px to ${ event.target.style.height }` );
});
.resizable {
border: 1px solid red;
height: 100px;
overflow: hidden;
resize: vertical;
width: 100px;
}
<div class="resizable"></div>
Only mighty want to add a check now to not run the function when something else inside the div is clicked.
I am trying to use drag and drop to move pictures from one <div> to another.
Currently, I can move the pictures anywhere in the destination <div>, but what I really want is that the pictures snap together when dropped. Ideally, they would be able to snap together on any side (not just, for example, on the bottom or on the right).
I've tried a few different things (including using <canvas>) and it didn't work.
This is what I have so far:
var clone;
var offsetx = null;
var offsety = null;
var isClone = false;
function allowDrop(ev) {
ev.preventDefault();
}
function drag(ev) {
offsetx = ev.target.offsetLeft - event.clientX;
offsety = ev.target.offsetTop - event.clientY;
ev.dataTransfer.setData("text", ev.target.id);
}
function dropTrash(ev) {
ev.preventDefault();
var data = ev.dataTransfer.getData("text");
var remove = document.getElementById(data);
remove.parentNode.removeChild(remove);
}
function drop(ev) {
ev.preventDefault();
var data = ev.dataTransfer.getData("text");
}
function dropClone(ev) {
ev.preventDefault();
var data = ev.dataTransfer.getData("text");
var num = Math.random() * (1000 - 1) + 1;
isClone = true;
clone = document.getElementById(data).cloneNode(true);
clone.id = "newId" + num.toString();
clone.style.position = "absolute";
clone.style.left = (event.clientX+offsetx)+"px";
clone.style.top = (event.clientY+offsety)+"px";
ev.target.appendChild(clone);
}
html, body {
height: 100%;
padding: 0;
margin: 0;
}
div {
width: 50%;
height: 50%;
float: left;
}
#div1 {
background: #DDD;
}
#div2 {
background: #AAA;
}
#div3 {
background: #777;
}
#div4 {
background: #444;
}
#imgDiv {
width: 611px;
height: 324px;
border: 5px solid #DDD;
}
<div id="div1">
</div>
<div id="div2">
</div>
<div id="div3" ondrop="dropTrash(event)" ondragover="allowDrop(event)">
<img id="drag1" src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/Bartagame_fcm.jpg/1200px-Bartagame_fcm.jpg" draggable="true" ondragstart="drag(event)" width="105" height="105">
<img id="drag2" src="http://www.earthtimes.org/newsimage/lizard_Ngo_Van_Tri_big_281.jpg" draggable="true" ondragstart="drag(event)" width="105" height="105">
</div>
<div id="div4">
<div align="center" id="imgDiv" ondrop="dropClone(event)" ondragover="allowDrop(event)"></div>
</div>
When you start dragging an image, you need to store the position of the cursor, relative to that particular image.
There are multiple position properties in the MouseEvent that will help you calculate that, but if browser support is not an issue, I would go for MouseEvent.offsetX and MouseEvent.offsetY. From the docs:
The offsetX/offsetY read-only property of the MouseEvent interface provides the offset in the X/Y coordinate of the mouse pointer between that event and the padding edge of the target node.
So, on dragstart, you will do just:
x = e.offsetX;
y = e.offsetY;
Then, when you drop the image in your, let's call it canvas (note the italics as it is not a <canvas> element, but any other element that you use as your drop zone, a <div> in this particular example), you need to know the position of the cursor relative to that canvas, so you may think you could use offsetX and offsetY again, and your are partially right. That would give you the expected value if you drop the image on the canvas itself, but there may be other images in it already, and you may drop the current one on top of another one, getting the offsetX and offsetY relative to that one instead.
What you can do is use MouseEvent.pageX and MouseEvent.pageY and, from that value, subtract the position of the (upper-left corner) of that canvas element, which you can get from HTMLElement.offsetLeft and HTMLElement.offsetTop:
e.pageX - imageCanvas.offsetLeft;
e.pageY - imageCanvas.offsetTop;
With this you will get the position of the cursor relative to the canvas element.
Now, you need to subtract the x and y values that you stored on dragstart, and that will give you the left and top values of the upper-left corner of the dragged image, relative to the canvas element:
image.style.left = (e.pageX - imagesCanvas.offsetLeft - x) + 'px';
image.style.top = (e.pageY - imagesCanvas.offsetTop - y) + 'px';
All together it will look like this:
let x;
let y;
let currentTarget = null;
let cloneElement = false;
function startDrag(e, clone) {
const target = e.target;
if (target.tagName === 'IMG') {
x = e.offsetX;
y = e.offsetY;
currentTarget = target;
cloneElement = clone;
}
}
function cloneImage(e) {
startDrag(e, true);
}
function moveImage(e) {
startDrag(e, false);
}
function removeImage(e) {
if (!cloneElement) {
currentTarget.remove();
}
}
function stickImage(e) {
const image = cloneElement ? currentTarget.cloneNode(true) : currentTarget;
imagesCanvas.appendChild(image);
// + 1 for the border
image.style.left = (e.pageX - imagesCanvas.offsetLeft - x + 1) + 'px';
image.style.top = (e.pageY - imagesCanvas.offsetTop - y + 1) + 'px';
currentTarget = null;
}
function allowDrag(e) {
e.preventDefault();
}
// Bind event listeners:
const imagesBarElement = document.getElementById('imagesBar');
const imagesCanvasElement = document.getElementById('imagesCanvas');
document.addEventListener('dragenter', allowDrag);
document.addEventListener('dragover', allowDrag);
imagesBarElement.addEventListener('dragstart', cloneImage);
imagesBarElement.addEventListener('drop', removeImage);
imagesCanvasElement.addEventListener('dragstart', moveImage);
imagesCanvasElement.addEventListener('drop', stickImage);
body {
margin: 0;
font-size: 0;
display: flex;
flex-direction: column;
height: 100vh;
user-select: none;
}
img {
width: 100px;
height: 100px;
}
#imagesBar {
height: 100px;
border-bottom: 1px solid #CCC;
padding: 10px 0;
}
#imagesBar > img {
margin: 0 0 0 10px;
}
#imagesCanvas {
position: relative;
background: #EEE;
flex-grow: 1;
overflow: hidden;
}
#imagesCanvas > img {
position: absolute;
}
<div id="imagesBar">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/Bartagame_fcm.jpg/1200px-Bartagame_fcm.jpg" draggable="true">
<img src="http://www.earthtimes.org/newsimage/lizard_Ngo_Van_Tri_big_281.jpg" draggable="true">
</div>
<div id="imagesCanvas"></div>