Simulating drag and drop on a SortableJS sortable list using JavaScript
Asked Answered
D

2

12

I'm attempting to simulate a drag and drop action on a sortable HTML list created using the Sortable library. It uses the native HTML5 API to implement draggable elements and sorting within a list.

To simulate these drag events, I found and modified the following JavaScript code:

var triggerSortableDragAndDrop = function (selectorDrag, selectorDrop, callback) {
  var DELAY_INTERVAL_MS = 10;
  var MAX_TRIES = 2;

  // fetch target elements
  var elemDrag = document.querySelector(selectorDrag);
  var elemDrop = document.querySelector(selectorDrop);
  elemDrag.setAttribute('draggable',"true");
  elemDrop.setAttribute('draggable',"true");
  elemDrag.href="#";
  
  var dragItems = document.querySelectorAll('[draggable=true]');


  if (!elemDrag || !elemDrop) {
    console.log("can't get elements");
    return false;
  }

  var startingDropRect = elemDrop.getBoundingClientRect();

  function rectsEqual(r1, r2) {
    return r1.top === r2.top && r1.right === r2.right && r1.bottom === r2.bottom && r1.left === r2.left;
  }

  // function for triggering mouse events
  function fireMouseEvent(type, elem) {
    var evt = document.createEvent('MouseEvent');
    evt.initMouseEvent(type, true, true, window, 1, 1, 1, 0, 0, false, false, false, false, 0, elem);
    elem.dispatchEvent(evt);
  };

  // trigger dragging process on top of drop target
  // We sometimes need to do this multiple times due to the vagaries of
  // how Sortable manages the list re-arrangement
  var counter = 0;
  function dragover() {
    counter++;
    console.log('DRAGOVER #' + counter);

    var currentDropRect = elemDrop.getBoundingClientRect();
    if (rectsEqual(startingDropRect, currentDropRect) && counter < MAX_TRIES) {
      if (counter != 1) console.log("drop target rect hasn't changed, trying again");

      // mouseover / mouseout etc events not necessary
      // dragenter / dragleave events not necessary either
      fireMouseEvent('dragover', elemDrop);

      setTimeout(dragover, DELAY_INTERVAL_MS);
    } else {
      if (rectsEqual(startingDropRect, currentDropRect)) {
        console.log("wasn't able to budge drop target after " + MAX_TRIES + " tries, aborting");
        fireMouseEvent('drop', elemDrop);
        if (callback) callback(false);
      } else {
        setTimeout(drop, DELAY_INTERVAL_MS);
      }
    }
  }

  function drop() {
    console.log('DROP');
    // release dragged element on top of drop target
    fireMouseEvent('drop', elemDrop);
    fireMouseEvent('mouseup', elemDrop);    // not strictly necessary but I like the symmetry
    if (callback) callback(true);
  }

  // start dragging process
  console.log('DRAGSTART');
  fireMouseEvent('mousedown', elemDrag);
  console.log('mousedown triggered');
  fireMouseEvent('dragstart', elemDrag);
  console.log('dragstart triggered');

  // after a delay, do the first dragover; this will run up to MAX_TRIES times
  // (with a delay between each run) and finally run drop() with a delay:
  setTimeout(dragover, DELAY_INTERVAL_MS);
  return true;
};

And the markup of the section I'm trying to drag and drop with is as follows:

enter image description here

When I tried to set breakpoints on the browser's drag event listeners, and execute the helper function in my browser console using:

triggerSortableDragAndDrop('#bookmarkItems > li:nth-child(2)', '#bookmarkItems > li:nth-child(2)');

I noticed that the dragstart event was never captured, but the mousedown and dragover events were.

How can I get the dragstart event fire to trigger its listener? Because I think that is what's causing the drag and drop simulation to fail.

Disgusting answered 15/3, 2020 at 7:24 Comment(6)
I am facing a similar issue, did you ever manage to get this resolved?Shrovetide
@Shrovetide Nope I've given up on it :P Let me know if you find a working solutionDisgusting
I ended up using CypressJS for drag and drop events. Works with npmjs.com/package/@4tw/cypress-drag-dropShrovetide
@Shrovetide Wish I had that option :/ I'm only working with C#/Specflow and IE - thanks thoughDisgusting
@Disgusting Is the url public? Would you consider a solution in Selenium-Python or Selenium_Java?Theotokos
Hi @Disgusting , can you give me some feedback about my answer?Upbow
U
6

I can see in your code the dragstart event is created of type MouseEvent while it is of type DragEvent.

var elem = document.getElementById("one");
var mousedown = document.createEvent('MouseEvent');
mousedown.initMouseEvent("mousedown", true, true, window, 1, 1, 1, 0, 0, false, false, false, false, 0, elem);
elem.dispatchEvent(mousedown);
var dragstart = document.createEvent('DragEvent');
dragstart.initMouseEvent("dragstart", true, true, window, 1, 1, 1, 0, 0, false, false, false, false, 0, elem);
elem.dispatchEvent(dragstart);
<div id="one" draggable="true" onmousedown="console.log('mousedown')" ondragstart="console.log('dragstart')">drag me</div>

Creating the dragstart as an event of the correct type, at least on Chrome and Edge, it works.

Hope this helps.

Upbow answered 29/6, 2020 at 0:41 Comment(0)
J
2

I used a more modern approach to trigger the dragstart and mousedown events (note that using event constructors is preferable over document.createEvent()). Both events work as expected. Some code to illustrate that below:

let text = document.getElementById('image');
text.addEventListener('dragstart', () => {
  console.log('dragstart triggered')
});
text.addEventListener('mousedown', () => {
  console.log('mousedown triggered')
});

function btn_click() {
  const evt_1 = new MouseEvent('mousedown');
 text.dispatchEvent(evt_1);
  
  const evt_2 = new DragEvent('dragstart');
 text.dispatchEvent(evt_2);
}
<p>Drag around the image to trigger the ondragstart and mousedown event.</p>
<button onclick='btn_click()'>Programatically trigger the ondragstart and onmousedown events.</button>
<br>
<br>
<img id='image' src='https://via.placeholder.com/150'>

However there is a big unknown in your code; How does the library you are using handle these events? Does it even handle the ondragstart event? It may very well only use other events, thus you can't assume these events in your JS code. The main issue is the coupling between your JS and that in the library.

So there are 2 ways to go about this

  1. Ensure your code uses the events the library responds to (keep in mind that the code in the library and the available events may change over time)
  2. Implement your own drag-and-drop behaviour

PS: If you are writing tests, you may decide not to test the library and trust it to behave as expected. Maybe you can validate whether your implementation of the library is correct instead.

Johnajohnath answered 2/7, 2020 at 11:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.