拖放 API 和表格行(仅限 vanilla JS)

Posted

技术标签:

【中文标题】拖放 API 和表格行(仅限 vanilla JS)【英文标题】:Drag & Drop API and table rows (vanilla JS only) 【发布时间】:2020-09-20 03:36:52 【问题描述】:

在我的一列n行的html表格中,例如:

<table id="my-table">
  <tr>
    <td>How</td>
  </tr>
  <tr>
    <td>Are</td>
  </tr>
  <tr>
    <td>You</td>
  </tr>
</table>

我想让每一行都可以拖放到另一行。如果将 A 行放到另一行 B 中,则应交换这些相应行的 HTML 内容。

为此,我阅读了相当多的文档和教程,但似乎缺少一些我无法弄清楚的东西。到目前为止,我所做的是将以下代码附加到每个 &lt;td&gt;tag 中:

draggable="true" ondragstart="dragCell(event)" ondragover="allowDrop(event)" ondrop="dropCell(event)" ondragenter="handleDragEnter(event)" ondragleave="handleDragLeave(event)" ondragend="handleDragEnd(event)"

我的 javascript 是:

/*##############################################################################
##                            1. Dragstart Handler                            ##
##############################################################################*/

function dragCell(ev) 
  let draggedRow = ev.target.closest("tr");
  let rowNumber = draggedRow.rowIndex;
  let tableId = draggedRow.parentNode.parentNode.getAttribute("id");
  let data = "id":tableId,"rowNumber":rowNumber;
  let cellsdragged = draggedRow.children;
  let amountOfCells = cellsdragged.length;
  let dataToTransfer = [];
  for (let i = 0; i < amountOfCells; i++) 
    let currentCell = cellsdragged[i];
    dataToTransfer.push(currentCell.outerHTML);
  
  data["cellContents"] = dataToTransfer;
  data = JSON.stringify(data);
  ev.dataTransfer.setData("text/plain",data);
  document.getElementsByTagName("body")[0].style.cursor = "grabbing";


/*##############################################################################
##                             2. Dragover Handler                            ##
##############################################################################*/

function allowDrop(ev) 
  ev.preventDefault();


/*##############################################################################
##                               3. Drop Handler                              ##
##############################################################################*/

function dropCell(ev) 
  ev.preventDefault();
  if (ev.stopPropagation) 
    ev.stopPropagation();
  
  document.getElementsByTagName("body")[0].style.cursor = "default";
  let data = ev.dataTransfer.getData("text/plain");
  data = JSON.parse(data);
  let rowToDrop = ev.target.closest('tr');
  let targetIndex = rowToDrop.rowIndex;
  let targetId = rowToDrop.parentNode.parentNode.getAttribute("id");
  if (data.id == targetId && data.rowNumber != targetIndex) 
    let targetContents = [];
    let cellsForDrop = rowToDrop.children;
    let amountOfCells = cellsForDrop.length;
    for (let i = 0; i < amountOfCells; i++) 
      targetContents
        .push(cellsForDrop[i].outerHTML);
    
    let draggedRow = document.getElementById(data.id).rows[data.rowNumber];
    let cellsOfDrag = draggedRow.children;
    for (let i = 0; i < amountOfCells; i++) 
      cellsForDrop[i].outerHTML = data.cellContents[i][0];
      cellsOfDrag[i].outerHTML = targetContents[i][0];
    
  


/*##############################################################################
##                             4. Dragenter Handler                           ##
##############################################################################*/

function handleDragEnter(ev) 
  ev.target.closest('tr').classList.add('ready-for-drop');


/*##############################################################################
##                            5. Dragleave Handler                            ##
##############################################################################*/

function handleDragLeave(ev) 
  ev.target.closest('tr').classList.remove('ready-for-drop');


/*##############################################################################
##                              6. Dragend Handler                            ##
##############################################################################*/

function handleDragEnd(event) 
  let rows = ev.target.closest("table").rows;
  let amount = rows.length;
  for (let i = 0; i < amount; i++) 
    if (rows[i].classList.contains('ready-for-drop')) 
      rows[i].classList.remove('ready-for-drop')
    
  

当我运行此程序并将一行拖入另一行时,控制台为我提供了有关 ev.target.closest 的错误,对于 Dragenter、Dragleave 和 Dragend Handler 函数,总是说 ev.target.closest is not a function。尽管我在 dragstart 处理程序函数中使用完全相同的语法,但在该行的控制台中没有报告错误。完成拖拽后,拖拽行的内容都变为&lt;

我在这里缺少什么?请考虑我不想使用任何 jQuery 解决方案,我想坚持使用 vanilla JavaScript。另请考虑我确实需要交换整个outerHTML,因为我的&lt;td&gt; 标签可能包含特定于单元格的属性值。

【问题讨论】:

【参考方案1】:

好的,想出了一个解决方案,最重要的事实:

我使用addEventListener而不是HTML标签内的HTML内联事件监听器,如上所述,有几个原因(我不会在这里进一步说明)

我遇到的最棘手的问题是,当我将被拖动的元素拖到绑定了 dragenter 和 dragleave 侦听器的元素的子元素上时,我的 dragleave 事件函数被触发。我在这个论坛上阅读了无数关于这个问题的文章和帖子,并且提出的解决方案似乎都不能轻松或简单地与我非常基本的 HTML 表格行和单元格一起工作,并且很快就使用函数来停止事件传播等,实际上并非如此甚至是必要的,正如您将在我的代码中看到的那样。此外,我在我的解决方案中完全省略了 dragleave 事件侦听器,所以这可能是实现我想要的更简单的方法之一,这就是为什么我想在这里分享它给其他可能想做我想做的类似事情的人.

与上面的不同之处在于:

HTML:我在td 标签中只包含了draggable="true" class="table-cell"

JavaScript:现在完全不同,但运行良好:

/*##############################################################################
##    1. Define Function which adds all the required event listeners          ##
##############################################################################*/

function addDragEvents(element) 
  element.addEventListener("dragstart",dragCell);
  element.addEventListener("dragover",allowDrop);
  element.addEventListener("drop",dropCell);
  element.addEventListener("dragenter",handleDragEnter);
  element.addEventListener("dragend",handleDragEnd);


/*##############################################################################
##    2. Define Function which resets the drag state for the concerned table  ##
##############################################################################*/

function resetDragState(e) 
  let data = e.dataTransfer.getData("text/plain");
  data = JSON.parse(data);
  let rows = document.getElementById(data.id).rows;
  for (let row of rows) 
    if (row.classList.contains('ready-for-drop')) 
      row.classList.remove('ready-for-drop');
    
    let cells = row.cells;
    for (let cell of cells) 
      cell.style.cursor="pointer";
    
  


/*##############################################################################
##    3. Add Required Event listeners to all table cells                      ##
##############################################################################*/

let cells = document.getElementsByClassName('table-cell');
for (let cell of cells) 
  addDragEvents(cell);


/*##############################################################################
##                            4. Dragstart Handler                            ##
##############################################################################*/

// This function is created to define the action of a drag (which data will be
// taken for transfer to the HTML element into which the dragged element's
// content will be dropped)

function dragCell(e) 
  // Change the cursor to a grabbing hand on dragstart
  let cells = document.getElementsByClassName('table-cell');
  for (let cell of cells) 
    cell.style.cursor="grabbing";
  
  // Three pieces of information must be transferred for the drag & drop to work
  // properly: the table id of the table having the row being dragged (to assure
  // that D&D only works among rows of the same table), the row index of the row
  // being dragged (to know which row needs to be replaced via the drop
  // function), and finally the content of the row being dragged
  let draggedRow = e.target.closest("tr");
  // Get the row index of that row
  let rowNumber = draggedRow.rowIndex;
  // Get the id name of the table having that row
  let tableId = draggedRow.parentNode.parentNode.getAttribute("id");
  // Initiate JSON object which will be transferred to the drop row
  let data = "id":tableId,"rowNumber":rowNumber;
  // Append all the cells as second element onto this same object
  let cellsdragged = draggedRow.children;
  let amountOfCells = cellsdragged.length;
  let dataToTransfer = [];
  for (let i = 0; i < amountOfCells; i++) 
    let currentCell = cellsdragged[i];
    dataToTransfer.push(currentCell.outerHTML);
  
  data["cellContents"] = dataToTransfer;
  data = JSON.stringify(data);
  e.dataTransfer.setData("text/plain",data);


/*##############################################################################
##                             5. Dragover Handler                            ##
##############################################################################*/

// This function is used to allow for drops into the corresponding HTML elements
// (the default behavior doesn't allow this)
function allowDrop(e) 
  e.preventDefault();


/*##############################################################################
##                               6. Drop Handler                              ##
##############################################################################*/

function dropCell(e) 
  // First, prevent default behavior once again
  e.preventDefault();
  // Second, access data coming from dragged element (which is the index of the
  // row from which data is being dragged)
  let data = e.dataTransfer.getData("text/plain");
  data = JSON.parse(data);
  // Next, get the index of the row into which content shall be dropped
  let rowToDrop = e.target.closest('tr');
  let targetIndex = rowToDrop.rowIndex;
  // Next, get the id of the table of that retrieved row
  let targetId = rowToDrop.parentNode.parentNode.getAttribute("id");
  // Next, only proceed if the dragged row comes from the same table as the
  // target row, and if the dragged and the target rows are two different rows
  if (data.id == targetId && data.rowNumber != targetIndex) 
    // Store the contents of the target row in the same array structure as the
    // one coming from the dragged row
    let targetContents = [];
    // Exchange the contents of the two rows
    let cellsForDrop = rowToDrop.children;
    let amountOfCells = cellsForDrop.length;
    for (let i = 0; i < amountOfCells; i++) 
      targetContents
        .push(cellsForDrop[i].outerHTML);
    
    // Exchange the contents of the two rows
    let draggedRow = document.getElementById(data.id).rows[data.rowNumber];
    let cellsOfDrag = draggedRow.children;
    for (let i = 0; i < amountOfCells; i++) 
      // Replace the content of the row into which the drag is being dropped
      // with the content of the dragged row
      cellsForDrop[i].outerHTML = data.cellContents[i];
      // Replacement of the outerHTML deletes all bound event listeners, so:
      addDragEvents(cellsForDrop[i]);
      // And now, replace the content of the dragged row with the content of the
      // target row. Then, do the same for the value.
      cellsOfDrag[i].outerHTML = targetContents[i];
      addDragEvents(cellsOfDrag[i]);
    
    resetDragState(e);
  


/*##############################################################################
##                             7. Dragenter Handler                           ##
##############################################################################*/

// When dragging over the text node of a table cell (the text in a table cell),
// while previously being over the table cell element, the dragleave event gets
// fired, which stops the highlighting of the currently dragged cell. To avoid
// this problem and any coding around to fight it, everything has been
// programmed with the dragenter event handler only; no more dragleave needed

// For the dragenter event, e.target corresponds to the element into which the
// drag enters. This fact has been used to program the code as follows:

var previousRow = null;

function handleDragEnter(e) 
  // Assure that dragenter code is only executed when entering an element (and
  // for example not when entering a text node)
  if (e.target.nodeType === 1) 
    // Get the currently entered row
    let currentRow = this.closest('tr');
    // Check if the currently entered row is different from the row entered via
    // the last drag
    if (previousRow !== null) 
      if (currentRow !== previousRow) 
        // If so, remove the class responsible for highlighting it via CSS from
        // it
        previousRow.className = "";
      
    
    // Each time an HTML element is entered, add the class responsible for
    // highlighting it via CSS onto its containing row (or onto itself, if row)
    currentRow.className = "ready-for-drop";
    // To know which row has been the last one entered when this function will
    // be called again, assign the previousRow variable of the global scope onto
    // the currentRow from this function run
    previousRow = currentRow;
  


/*##############################################################################
##                              8. Dragend Handler                            ##
##############################################################################*/

// This function is required for cases where the dragged has been dropped on a
// non-valid drop target.

function handleDragEnd(e) 
  resetDragState(e);

我将 cmets 留在了里面,因为我在网上苦苦挣扎(但没有成功)如何尽可能简单地使用拖放 API 使 HTML 表格的行/单元格可拖放,所以我希望这个回答会帮助别人!

【讨论】:

以上是关于拖放 API 和表格行(仅限 vanilla JS)的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Vanilla Js 拖放存储已放置项目的状态?

在 Vanilla JS 和 Django 中保护 API 密钥的最佳方法

如何通过 Protractor 使用 vanilla JS 从 API 获取 json

使用 jQueryUI 可排序(但仅限 Chrome)删除一行时,1 个额外的表格单元格

使用拖放重新排列 HTML 表格行

在angularjs中对表格的行进行排序或重新排列(拖放)