Search code examples
javascripthtmlcssdrag-and-drop

JavaScript Drag and Drop Swap Items


I'm trying to create a simple drag & drop example to use that will allow swapping of items. For example:

Item 0 Item 1 Item 2 Item 3

If I drag and drop "Item 0" over "Item 3" they should swap places. What I have below does not swap the correct elements, will also make some slots "un-droppable" and error out due to e.dataTransfer not providing any data.

const log = console.log.bind(console);
const $ = document.getElementById.bind(document);

function drop(e) {
  e.preventDefault();
  let dragindex = 0;
  let clone = e.target.cloneNode(true);
  let data = e.dataTransfer.getData("text/plain");


  if (clone.dataset.id !== data) {

    [...$("container").children].forEach((el, i) => {
      if (el.dataset.id == data) {
        dragindex = +i;
      }
    })

    log(data, clone.dataset.id, dragindex, e.target.dataset.id);

    $("container").replaceChild(document.querySelector(`[data-id=${data}]`), e.target);
    $("container").insertBefore(clone, $("container").childNodes[dragindex]);
  }
}

[...document.querySelectorAll(".draggable")].map((el) => {
  el.setAttribute("draggable", true);
});

[...document.querySelectorAll(".draggable")].map((el) => {
  el.addEventListener("dragover", (e) => {
    e.preventDefault();
  })
  el.addEventListener("dragstart", (e) => {
    e.dataTransfer.setData("text/plain", e.target.dataset.id);
  });

  el.addEventListener("drop", (e) => {
    drop(e);
  });
})
#container {
    width: 200px;
    height: auto;
    position: absolute;
    left: 50%;
    top: 50%;
    background: dodgerblue;
    color: #fff;
    transform: translate(-50%, -50%);
    border: 1px solid #000;
}

.draggable {
    display: flex;
    justify-content: start;
    align-items: center;
    border: 1px solid #fff;
    margin: 2px;
    padding: .5em;
    text-align: center;
    cursor: grab;
}

.draggable i {
    margin-right: 25px;
}
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<div id="container">
  <div class="draggable" data-id="drag0"><i class="material-icons">drag_indicator</i>Draggable 0</div>
  <div class="draggable" data-id="drag1"><i class="material-icons">drag_indicator</i>Draggable 1</div>
  <div class="draggable" data-id="drag2"><i class="material-icons">drag_indicator</i>Draggable 2</div>
  <div class="draggable" data-id="drag3"><i class="material-icons">drag_indicator</i>Draggable 3</div>
</div>


Solution

  • So.... I finally cracked it. It was several things.

    1. You have to re-add event listeners to a cloned node if they were added via "document.addEventListener()".

    2. Had to use ".childNodes" not ".children" as the indexing does not work out the same.

    3. Had to take care where/when I created the variable holding the reference node.

    Also figured out that it acts weird in Safari on IOS if the drag/drop parent container is absolutely positioned so need to use flex positioning, as well as a few other minor details; any how, in case any one finds it helpful, here is the working code. Works in Android-Chrome/IOS-Safari.

    const log = console.log.bind(console);
    const $ = document.getElementById.bind(document);
    
    function drop(e) {
      e.preventDefault();
      let dragindex = 0;
      let referenceNode = "";
      let clone = e.target.cloneNode(true);
      addListeners(clone);
      let data = e.dataTransfer.getData("text/plain");
    
      if (clone.dataset.id !== data) {
        [...$("container").childNodes].forEach((el, i) => {
          if (el.dataset?.id == data) {
            dragindex = i;
          }
        });
    
        $("container").replaceChild(
          document.querySelector(`[data-id=${data}]`),
          e.target
        );
        referenceNode = $("container").childNodes[dragindex];
        $("container").insertBefore(clone, referenceNode);
        clone.classList.remove("dragActive");
      }
    }
    
    function addListeners(el) {
      el.addEventListener("dragover", (e) => {
        e.preventDefault();
        e.target.classList.add("dragActive");
      });
      el.addEventListener("dragstart", (e) => {
        e.dataTransfer.setData("text/plain", e.target.dataset.id);
      });
      el.addEventListener("dragleave", (e) => {
        e.target.classList.remove("dragActive");
      });
      el.addEventListener("dragend", (e) => {
        e.target.classList.remove("dragActive");
      });
      el.addEventListener("drop", (e) => {
        e.target.classList.remove("dragActive");
        drop(e);
      });
    }
    
    [...document.querySelectorAll(".draggable")].map((el) => {
      el.setAttribute("draggable", true);
    });
    
    [...document.querySelectorAll(".draggable")].map((el) => {
      addListeners(el);
    });
    html,
    body {
        margin: 0;
        padding: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        width: 100%;
        height: 100%;
    }
    
    #container {
        width: 200px;
        height: auto;
        background: dodgerblue;
        color: #fff;
        border: 1px solid #000;
        position: absolute;
        /* top: 50%;
        left: 50%;
        transform: translate(-50%, -50%); */
    }
    
    .draggable {
        border: 1px solid #fff;
        margin: 2px;
        padding: .5em;
        text-align: center;
        cursor: grab;
        color: #000;
        background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAzCAYAAAAdD7HCAAAACXBIWXMAAA7DAAAOwwHHb6hkAAADGElEQVRYhe1Zy2oUURA9MSoiWYQRxEdAMBoDxpVGZhFQF5qV+AgSPyK6CPgFbswvCD4WrkQQHwsFDUY3JuomISjic8CZhYk6PtCoUev07Rlm2p7uPj2TXR84zUxSValO31t16nYb6rHM2G/sMf40PjG+QGvQaTxo7DX+Nk4Z7xp/hBkfMr4y/g1w3LityURGjV9CYheMg0HjkRDDWpaNO1MmMhYTexHuH+Ghz/grxoF8aVwpJjKQIC45b8zR4VJCB/K4mMxVIfYJLtg9QvBBMZndgu1eJtMlOKyFhpxg28FkCoJDCRreCLZvmcwdweEmNFwTbK/wssW4gPgFNgNXFBXwMb1PEHui1mkY0du75CedBnnjx4jY08Z1QSfuqsmAIRO8DG2Rh2GT8SLqq3DReNq4umLUFuK42dgN1z+Y9TxaBxbN9X7sd8iQIcMSIWxrrzH2+p+foXVbm9V7l3GH/30WTnr+CTOmtGTvofKqLXrXjd1NJrLP/+PB6jvr/64OLNllNC7ZLOdpZecQolsNb/5YxbgDrvfENbOSb6uAPaecIPY32rbb5aTxaILATGTO+FBI5pTxQAK7FXCjER4guU69DQ1K7EmucEUabhCT6RRsPdn5VXD4AA1FwXZOlZ33oOGWYHuDF654rua4Z1pGiCKLQaqdStm5GGHMOjGEdOBBQpTs5E3mg05SlRSxFa6S1xY/3jwrfvVQIexIhFPgdv87J4LHaNA/UmCp+l6GDBnq0N7g5xvhGuh3uHrQKnBrs3T0+HHLjQw583L2ZXOrFCbOxufhZuVmwFmdM3tQ8XG2/+/kjD1nGo1LNotTPmUiPL2I6k9McLjWYQLxzYznLDkxEVb0mQSxF/ykPVmYVI2dEZM5LMQ+R4ezgsNTMZkLQmzvTE9ZnOqhkSJTu1TZ+RkaFNlZZDL3BYcpaFAkrXfIyB3CrZvkuQ6IyfDY7HWCuNzefRWnI4iWneQY0oFjcdxUORJ04nuBQoghq/AomgOl5XhIbL7fqr7eCcrOVcb9cEcXy+GkIUeIT2gNWNj6/djPjY9QlbTAPz6/t2nPvICTAAAAAElFTkSuQmCC');
        background-repeat: no-repeat;
        background-size: 15px;
        background-position: 5px;
        position: relative;
    }
    
    .dragActive {
        background: rgba(255, 255, 255, .25);
        color: #000;
        border: 1px solid #000;
    }
    <div id="container">
      <div class="draggable" data-id="drag0">Draggable 0</div>
      <div class="draggable" data-id="drag1">Draggable 1</div>
      <div class="draggable" data-id="drag2">Draggable 2</div>
      <div class="draggable" data-id="drag3">Draggable 3</div>
    </div>