You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

jQuery Sortable拖拽多元素时ui.item.index()返回-1问题

jQuery Sortable跨表格拖拽ui.item.index()返回-1问题排查与解决

问题现象

用jQuery Sortable实现两个表格的多元素拖拽功能时,从table2拖拽元素到table1,ui.item.index()偶尔返回-1。仅当拖拽起始位置是table2的索引0时正常,其他位置(如索引2)必现此异常,且测试环境无法复现。

相关代码片段

Table1的Sortable配置

$("#table1 tbody").sortable({
    connectWith: "#table2 tbody",
    helper: function (e, item) {
      const $helper = $("<div/>");
      const $originals = item.children();

      // 选中当前拖拽的行
      table1Api.row(item).select();

      const data = item.parent().children(":has(input:checked)").clone();
      matchHelperSizeToOriginal(data, $originals);

      // 隐藏带选中输入框的兄弟元素并存储数据
      item.data("data", data).siblings(":has(input:checked)").hide();

      return $helper.append(data);
    },
    start: function (e, ui) {
      const data = ui.item.data("data");
      ui.helper.append(data);
    },
    stop: function (e, ui) {
      // 无关代码
    },
    receive: function (e, ui) {
      const data = ui.item.data("data");
      let newIndex = ui.item.index(); // 此处返回-1
      console.log(newIndex);
      for (const d of data) {
        table1.row().add(d).draw();
      }
    },
    remove: function (e, ui) {
      const data = ui.item.data("data");
      for (const d of data) {
        table2.row(d).remove().draw();
      }
    },
    axis: "xy",
    cursor: "grabbing",
  });

Table2的Sortable配置

$("#table2 tbody").sortable({
    connectWith: "#table1 tbody",
    items: "tr:not(:has(.dataTables_empty))",
    helper: function (e, item) {
      const $helper = $("<div/>");
      const $originals = item.children();

      // 选中当前拖拽的行
      table2Api.row(item).select();

      const data = item.parent().children(":has(input:checked)").clone();
      matchHelperSizeToOriginal(data, $originals);

      // 隐藏带选中输入框的兄弟元素并存储数据
      item.data("data", data).siblings(":has(input:checked)").hide();

      return $helper.append(data);
    },
    start: function (e, ui) {
      const data = ui.item.data("data");
      ui.helper.append(data);
    },
    stop: function (e, ui) {
      ui.item.siblings(":hidden").show();
    },
    receive: function (e, ui) {
      const data = ui.item.data("data");
      for (const d of data) {
        table2.row().add(d).draw();
      }
    },
    remove: function (e, ui) {
      const data = ui.item.data("data");
      for (const d of data) {
        table2.row(d).remove().draw();
      }
    },
    axis: "xy",
    cursor: "grabbing",
  });

问题原因分析

  1. DOM结构干扰:拖拽时用hide()隐藏原容器中选中的兄弟元素,会将这些元素从DOM正常流中移除,打乱节点顺序计数。当拖拽非索引0的元素时,隐藏元素会导致index()方法无法正确计算目标位置。
  2. 元素插入时机问题:receive事件触发时,ui.item可能还未完全插入到目标容器的DOM结构中,此时调用index()会返回无效值-1。

解决方案

1. 替换元素隐藏方式,避免干扰DOM索引

不要用hide()隐藏元素,改用添加类名的方式标记,保留元素在DOM中的位置:

// Helper函数中修改隐藏逻辑
item.data("data", data).siblings(":has(input:checked)").addClass("drag-hidden");

// Table2的stop事件中恢复显示
stop: function (e, ui) {
  ui.item.siblings(".drag-hidden").removeClass("drag-hidden");
}

同时添加CSS样式:

.drag-hidden {
  visibility: hidden; /* 用visibility而非display,保留DOM节点位置 */
}

2. 用占位符索引替代ui.item.index()

receive事件中,ui.placeholder是拖拽时的占位元素,它的位置就是最终插入位置,用其索引获取正确值:

receive: function (e, ui) {
  const data = ui.item.data("data");
  // 获取占位符的索引作为插入位置
  let newIndex = ui.placeholder.index();
  console.log(newIndex);
  
  // 批量添加数据并插入到指定位置
  data.forEach((itemData, idx) => {
    // 添加行但不立即重绘
    const newRow = table1.row.add(itemData).draw(false);
    // 将新行插入到目标位置
    const targetNode = table1.row(newIndex + idx).node() || null;
    newRow.node().parentNode.insertBefore(newRow.node(), targetNode);
  });
  // 最终重绘表格
  table1.draw();
}

3. 脱离DOM索引依赖,直接操作DataTables数据

如果不需要精确控制插入位置,可直接添加数据后让DataTables自行处理;或者通过DataTables API获取目标容器行数计算位置,完全不依赖DOM索引。

内容的提问来源于stack exchange,提问作者user23454796

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 00:56:03