Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swap places on moving on element like in Windows taskbar (pure JavaScript)

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
like image 598
mmaismma Avatar asked Aug 21 '20 15:08

mmaismma


1 Answers

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.

like image 61
mmaismma Avatar answered Nov 17 '22 07:11

mmaismma