1

In brief, I want to take a single line with multiple buttons, input text fields, and text, and drag that line up and/or down in a list of similar lines, without being able to mess up the line itself. I'd like to use HTML, CSS, and Javascript only, without importing libraries.

==

In HTML, I have a div that contains multiple buttons, input text fields, and text. A number of these divs make an informal list. All these divs are, in turn, inside another div.

I want to take one of these internal divs and drag it up or down so it inserts into a new place in the list. (Not swapping with another informal-list element, but inserting into the list and pushing later elements down).

It is not important that the information be contained in divs; a table or whatever else is fine, so long as all the buttons/text fields/etc. are on the same line and can all be dragged around together.

==

I originally put each button, text field, etc. in a td, which was in turn inside a tr, which was inside a table, and did something similar to

https://www.tutorialspoint.com/Create-a-draggable-paragraph-in-HTML5

But when I dragged a line, the entire moved tr line would sometimes drop into another line's td element, which was not desired behavior. I wanted the entire tr line to move above or below another tr, not to squeeze into a single cell element of another tr.

So I tried doing things using this approach

https://htmldom.dev/drag-and-drop-element-in-a-list/

Currently, if I drag a button, the button can be shifted left and right within the internal div element it is in. This is not desired behavior; I want the elements in the internal div to stay in the same order, and to move the entire div, just as it is, up or down in the informal list of similar divs.

HTML below:

let draggingEle;

// The current position of mouse relative to the dragging element
let x = 0;
let y = 0;

const mouseDownHandler = function(e) {
  draggingEle = e.target;

  // Calculate the mouse position
  const rect = draggingEle.getBoundingClientRect();
  x = e.pageX - rect.left;
  y = e.pageY - rect.top;

  // Attach the listeners to `document`
  document.addEventListener('mousemove', mouseMoveHandler);
  document.addEventListener('mouseup', mouseUpHandler);
};

const mouseMoveHandler = function(e) {
  // Set position for dragging element
  draggingEle.style.position = 'absolute';
  draggingEle.style.top = `${e.pageY - y}px`;
  draggingEle.style.left = `${e.pageX - x}px`;

  const draggingRect = draggingEle.getBoundingClientRect();

  if (!isDraggingStarted) {
    // Update the flag
    isDraggingStarted = true;

    // Let the placeholder take the height of dragging element
    // So the next element won't move up
    placeholder = document.createElement('div');
    placeholder.classList.add('placeholder');
    draggingEle.parentNode.insertBefore(
      placeholder,
      draggingEle.nextSibling
    );

    // Set the placeholder's height
    placeholder.style.height = `${draggingRect.height}px`;
  }

  // The current order:
  // prevEle
  // draggingEle
  // placeholder
  // nextEle
  const prevEle = draggingEle.previousElementSibling;
  const nextEle = placeholder.nextElementSibling;
  // User moves item to the top
  if (prevEle && isAbove(draggingEle, prevEle)) {
    // The current order    -> The new order
    // prevEle              -> placeholder
    // draggingEle          -> draggingEle
    // placeholder          -> prevEle
    swap(placeholder, draggingEle);
    swap(placeholder, prevEle);
    return;
  }
  // User moves the dragging element to the bottom
  if (nextEle && isAbove(nextEle, draggingEle)) {
    // The current order    -> The new order
    // draggingEle          -> nextEle
    // placeholder          -> placeholder
    // nextEle              -> draggingEle
    swap(nextEle, placeholder);
    swap(nextEle, draggingEle);
  }
};

const mouseUpHandler = function() {
  // Remove the position styles
  draggingEle.style.removeProperty('top');
  draggingEle.style.removeProperty('left');
  draggingEle.style.removeProperty('position');

  x = null;
  y = null;
  draggingEle = null;

  // Remove the handlers of `mousemove` and `mouseup`
  document.removeEventListener('mousemove', mouseMoveHandler);
  document.removeEventListener('mouseup', mouseUpHandler);

  // Remove the placeholder
  placeholder && placeholder.parentNode.removeChild(placeholder);
  // Reset the flag
  isDraggingStarted = false;
};

// Query the list element
const list = document.getElementById('list');

// Query all items
[].slice.call(list.querySelectorAll('.draggable')).forEach(function(item) {
  item.addEventListener('mousedown', mouseDownHandler);
});

let placeholder;
let isDraggingStarted = false;

const isAbove = function(nodeA, nodeB) {
  // Get the bounding rectangle of nodes
  const rectA = nodeA.getBoundingClientRect();
  const rectB = nodeB.getBoundingClientRect();

  return rectA.top + rectA.height / 2 < rectB.top + rectB.height / 2;
};

const swap = function(nodeA, nodeB) {
  const parentA = nodeA.parentNode;
  const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling;

  // Move `nodeA` to before the `nodeB`
  nodeB.parentNode.insertBefore(nodeA, nodeB);

  // Move `nodeB` to before the sibling of `nodeA`
  parentA.insertBefore(nodeB, siblingA);
};
<div id="list">
  <div class="draggable">
    <button onclick="deleteRow(event)">&#10005;</button>
    <button onclick="splitRow(event)">+</button>
    <button class="flagButton" onclick="cycleFlag(event)">US</button>
    <button class="unitButton" draggable="false">Unit 1</button>
    <input type="text" size="2" value="0" /> Row command number Row destination number Loop count number
  </div>
  <div class="draggable">
    <button onclick="deleteRow(event)">&#10005;</button>
    <button onclick="splitRow(event)">+</button>
    <button class="flagButton" onclick="cycleFlag(event)">US</button>
    <button class="unitButton" draggable="false">Unit 2</button>
    <input type="text" size="2" value="0" /> Row command number Row destination number Loop count number
  </div>
</div>

What I tried: To make an HTML element that contained multiple buttons and input text fields, and have that entire HTML element dragged up or down in a list of similar elements.

What I expected: HTML element to be dragged up and down.

What actually resulted: Approach 1: Dragged HTML element inside a cell of another HTML element (instead of above it or below it). Approach 2: Dragged HTML sub-elements of the element left and right within the element, also not desired (want to move the entire element up or down).

Nexo
  • 2,125
  • 2
  • 10
  • 20

1 Answers1

1

Change draggingEle = e.currentTarget;

See What is the difference between currentTarget and target property in JavaScript

let draggingEle;

// The current position of mouse relative to the dragging element
let x = 0;
let y = 0;

const mouseDownHandler = function(e) {
  draggingEle = e.currentTarget;
  draggingEle.classList.add('dragging');

  // Calculate the mouse position
  const rect = draggingEle.getBoundingClientRect();
  x = e.pageX - rect.left;
  y = e.pageY - rect.top;

  // Attach the listeners to `document`
  document.addEventListener('mousemove', mouseMoveHandler);
  document.addEventListener('mouseup', mouseUpHandler);
};

const mouseMoveHandler = function(e) {
  // Set position for dragging element
  draggingEle.style.position = 'absolute';
  draggingEle.style.top = `${e.pageY - y}px`;
  draggingEle.style.left = `${e.pageX - x}px`;

  const draggingRect = draggingEle.getBoundingClientRect();

  if (!isDraggingStarted) {
    // Update the flag
    isDraggingStarted = true;

    // Let the placeholder take the height of dragging element
    // So the next element won't move up
    placeholder = document.createElement('div');
    placeholder.classList.add('placeholder');
    draggingEle.parentNode.insertBefore(
      placeholder,
      draggingEle.nextSibling
    );

    // Set the placeholder's height
    placeholder.style.height = `${draggingRect.height}px`;
  }

  // The current order:
  // prevEle
  // draggingEle
  // placeholder
  // nextEle
  const prevEle = draggingEle.previousElementSibling;
  const nextEle = placeholder.nextElementSibling;
  // User moves item to the top
  if (prevEle && isAbove(draggingEle, prevEle)) {
    // The current order    -> The new order
    // prevEle              -> placeholder
    // draggingEle          -> draggingEle
    // placeholder          -> prevEle
    swap(placeholder, draggingEle);
    swap(placeholder, prevEle);
    return;
  }
  // User moves the dragging element to the bottom
  if (nextEle && isAbove(nextEle, draggingEle)) {
    // The current order    -> The new order
    // draggingEle          -> nextEle
    // placeholder          -> placeholder
    // nextEle              -> draggingEle
    swap(nextEle, placeholder);
    swap(nextEle, draggingEle);
  }
};

const mouseUpHandler = function() {
  // Remove the position styles
  draggingEle.style.removeProperty('top');
  draggingEle.style.removeProperty('left');
  draggingEle.style.removeProperty('position');
  draggingEle.classList.remove('dragging');

  x = null;
  y = null;
  draggingEle = null;

  // Remove the handlers of `mousemove` and `mouseup`
  document.removeEventListener('mousemove', mouseMoveHandler);
  document.removeEventListener('mouseup', mouseUpHandler);

  // Remove the placeholder
  placeholder && placeholder.parentNode.removeChild(placeholder);
  // Reset the flag
  isDraggingStarted = false;
};

// Query the list element
const list = document.getElementById('list');

// Query all items
[].slice.call(list.querySelectorAll('.draggable')).forEach(function(item) {
  item.addEventListener('mousedown', mouseDownHandler);
});

let placeholder;
let isDraggingStarted = false;

const isAbove = function(nodeA, nodeB) {
  // Get the bounding rectangle of nodes
  const rectA = nodeA.getBoundingClientRect();
  const rectB = nodeB.getBoundingClientRect();

  return rectA.top + rectA.height / 2 < rectB.top + rectB.height / 2;
};

const swap = function(nodeA, nodeB) {
  const parentA = nodeA.parentNode;
  const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling;

  // Move `nodeA` to before the `nodeB`
  nodeB.parentNode.insertBefore(nodeA, nodeB);

  // Move `nodeB` to before the sibling of `nodeA`
  parentA.insertBefore(nodeB, siblingA);
};
.draggable {
  cursor: grab;
}

.dragging {
  cursor: grabbing;
}
<div id="list">
  <div class="draggable">
    <button>&#10005;</button>
    <button>+</button>
    <button>US</button>
    <button draggable="false">Unit 1</button>
    <input type="text" size="2" value="0" /> Row command number Row destination number Loop count number
  </div>
  <div class="draggable">
    <button>&#10005;</button>
    <button>+</button>
    <button>US</button>
    <button draggable="false">Unit 2</button>
    <input type="text" size="2" value="0" /> Row command number Row destination number Loop count number
  </div>
</div>
ksav
  • 20,015
  • 6
  • 46
  • 66
  • 1
    Amazing. The solution mentioned worked; I'll have to do some reading to understand *exactly* why, but now I know what to look for. I'll upvote once I get 15 rep. – Aardvark Pepper Jan 03 '23 at 18:28
  • 1
    Check the linked question. There are some good answers there. – ksav Jan 03 '23 at 22:21