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

为何Todo列表拖拽交换函数对新增元素无效?

问题排查与修复:新增Todo项拖拽排序undefined错误

问题根源

  1. listItems数组类型不统一
    初始化时listItems存储的是<li>DOM元素,但新增Todo时,执行了listItems.push(todoText.textContent),把字符串存入数组。后续swapItems中尝试对字符串调用querySelector,直接抛出undefined错误。

  2. 新增元素未绑定拖拽事件
    addEventListeners()仅在页面初始化时执行一次,新增的<li>和可拖拽元素没有绑定dragstart、drop等事件,拖拽逻辑完全不生效。

  3. data-index未正确递增
    变量index2初始值为6,但新增Todo后没有执行index2++,导致所有新增项的data-index都是6,拖拽时索引计算混乱。

  4. DOM元素类名设置错误
    使用setAttribute('className', 'draggable')是错误的,DOM元素的类名属性是class,而非className(className是JS对象属性,对应DOM的class属性),正确做法是用classList.add()。

修复步骤与代码修正

1. 修正listItems存储逻辑

在newTodo函数中,将<li>元素而非文本内容推入listItems:

// 替换原错误代码:listItems.push(todoText.textContent);
listItems.push(todoLi);

2. 给新增元素绑定拖拽事件

单独封装绑定单个元素事件的函数,避免重复绑定已有元素的事件:

// 新增函数:绑定单个可拖拽项的事件
function bindDragEvents(draggableItem, listItem) {
  draggableItem.addEventListener('dragstart', dragStart);
  listItem.addEventListener('dragover', dragOver);
  listItem.addEventListener('drop', dragDrop);
  listItem.addEventListener('dragenter', dragEnter);
  listItem.addEventListener('dragleave', dragLeave);
}

// 在createList的forEach中替换原绑定逻辑:
bindDragEvents(listItem.querySelector('.draggable'), listItem);

// 在newTodo函数末尾,给新增元素绑定事件:
bindDragEvents(todo, todoLi);

3. 递增data-index

在newTodo函数开头执行index2++,确保每个新增项的索引唯一:

function newTodo(value) {
  index2++; // 新增这一行
  // ... 原代码
}

4. 修正类名设置方式

替换所有setAttribute('className', ...)为classList.add():

// createList函数中:
// listItem.setAttribute('className', 'draggable');
listItem.classList.add('draggable');

// newTodo函数中:
// todoLi.setAttribute('className', 'draggable');
todoLi.classList.add('draggable');

完整修正后的关键代码片段

// 新增绑定单个元素事件的函数
function bindDragEvents(draggableItem, listItem) {
  draggableItem.addEventListener('dragstart', dragStart);
  listItem.addEventListener('dragover', dragOver);
  listItem.addEventListener('drop', dragDrop);
  listItem.addEventListener('dragenter', dragEnter);
  listItem.addEventListener('dragleave', dragLeave);
}

function createList() {
  [...TodoList].forEach((todo, index) => {
    const listItem = document.createElement('li');
    listItem.setAttribute('data-index', index + 1);
    listItem.classList.add('draggable'); // 修正类名设置
    listItem.innerHTML = `
            <label for="showRatings${index + 1}" class="circle${index + 1} check">
            <input type="checkbox" class="checkbox input" name="showRatings${index + 1}" id="showRatings${index + 1}">
            </label>
            <span class="number">${index + 1+"-"}</span>
            <div class="draggable" draggable="true">
            <p class="todo-name" >${todo}</p>
            </div>`;
    listItems.push(listItem);
    draggableElement.appendChild(listItem);
    // 绑定事件
    bindDragEvents(listItem.querySelector('.draggable'), listItem);
  });
}

function newTodo(value) {
  index2++; // 递增索引
  const todo = document.createElement('div');
  const todoText = document.createElement('p');
  todoText.classList.add('todo-name')
  const todoCheckbox = document.createElement('input');
  const todoCheckBoxLabel = document.createElement('label');
  todo.setAttribute('draggable', true);
  todo.classList.add('draggable')
  const numberSpan = document.createElement('span');
  numberSpan.classList.add('number')
  const todoLi = document.createElement('li');
  todoLi.classList.add('draggable', 'list-item', 'not-crossed'); // 修正类名设置
  const toDosContainer = document.querySelector('.draggable-list')
  const Container = document.querySelector('.container-todo')
  todoCross = document.createElement('span');
  todoCross.classList.add('cross')

  todoLi.setAttribute('data-index', index2);

  numberSpan.textContent = "-";

  todoText.textContent = value;

  todoCheckbox.type = "checkbox";
  todoCheckbox.name = `showRatings${index2}`; // 修正name避免重复
  todoCheckBoxLabel.htmlFor = `showRatings${index2}`; // 修正for避免重复

  todoCheckBoxLabel.addEventListener('click', function(e) {
    if (todoCheckbox.checked) {
      todoCheckbox.checked = false;
      todoText.style.textDecoration = 'none';
      todoCheckBoxLabel.classList.remove('active');
      todoLi.classList.add('not-crossed')
      todoLi.classList.remove('thick-crossed')

    } else {
      todoCheckbox.checked = true;
      todoText.style.textDecoration = "line-through";
      todoCheckBoxLabel.classList.add('active');
      todoLi.classList.remove('not-crossed')
      todoLi.classList.add('thick-crossed')
    }
  });

  TodoList.push(todoText.textContent);
  listItems.push(todoLi); // 存储li元素而非文本
  console.log('lista', listItems);

  todoCross.textContent = 'X';
  todoCross.addEventListener('click', function(e) {
    // 移除时同步从listItems中删除
    const removeIndex = listItems.indexOf(todoLi);
    if (removeIndex !== -1) {
      listItems.splice(removeIndex, 1);
    }
    e.target.parentElement.parentElement.remove(); // 修正层级,因为todoLi是父元素
  });

  Container.classList.add('todos-container');
  todo.classList.add('todo');
  todoCheckbox.classList.add('checkbox')
  todoCheckBoxLabel.classList.add('circle', 'chk');

  todo.appendChild(todoCheckbox);
  todo.appendChild(todoCheckBoxLabel);
  todo.appendChild(numberSpan)
  todo.appendChild(todoText);
  todo.appendChild(todoCross);
  todoLi.appendChild(todo);
  toDosContainer.appendChild(todoLi);
  
  // 给新增元素绑定拖拽事件
  bindDragEvents(todo, todoLi);
}

额外优化点

  • 删除Todo时同步从listItems数组中移除对应元素,避免数组和DOM不一致
  • 修正新增项的checkbox的name和for属性,避免重复ID/name导致的交互问题
  • 可以使用事件委托替代逐个绑定事件,提升性能,比如给父元素draggable-list绑定dragover、drop等事件,无需给每个li单独绑定

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 22:35:20