Кастомный Drag and Drop

Кастомный Drag and Drop — реализация перетаскивания элементов средствами JavaScript через события mousedown/mousemove/mouseup (или Pointer Events API) с вычислением смещений и управлением позицией через CSS.

Зачем нужно

Встроенный HTML5 Drag and Drop API (draggable, dragstart, drop) имеет ограничения в кастомизации внешнего вида, плохо работает на мобильных устройствах и не всегда предсказуем. Кастомная реализация через mouse/pointer events даёт полный контроль над поведением, анимациями и поддержкой тач-экранов.

Где используется

  • Kanban-доски и todo-листы с перетаскиванием задач
  • Drag-and-drop загрузка файлов (кастомная зона)
  • Перестановка элементов в списке
  • Изменение размера панелей (resizable panels)

Реализация: перетаскиваемый элемент

function makeDraggable(element) {
  let startX, startY, startLeft, startTop;
  let isDragging = false;

  element.style.position = 'absolute';

  function onMouseMove(e) {
    if (!isDragging) return;
    const dx = e.clientX - startX;
    const dy = e.clientY - startY;
    element.style.left = `${startLeft + dx}px`;
    element.style.top  = `${startTop + dy}px`;
  }

  function onMouseUp() {
    isDragging = false;
    element.classList.remove('dragging');
    document.removeEventListener('mousemove', onMouseMove);
    document.removeEventListener('mouseup', onMouseUp);
  }

  element.addEventListener('mousedown', (e) => {
    e.preventDefault(); // запрещаем выделение текста
    isDragging = true;
    startX = e.clientX;
    startY = e.clientY;
    startLeft = element.offsetLeft;
    startTop  = element.offsetTop;
    element.classList.add('dragging');

    document.addEventListener('mousemove', onMouseMove);
    document.addEventListener('mouseup', onMouseUp);
  });
}

makeDraggable(document.getElementById('card'));

Реализация: сортируемый список

function makeSortable(list) {
  let dragged = null;

  list.addEventListener('dragstart', (e) => {
    dragged = e.target.closest('li');
    dragged.classList.add('dragging');
  });

  list.addEventListener('dragend', () => {
    dragged?.classList.remove('dragging');
    dragged = null;
  });

  list.addEventListener('dragover', (e) => {
    e.preventDefault(); // разрешить drop
    const target = e.target.closest('li');
    if (!target || target === dragged) return;

    const rect = target.getBoundingClientRect();
    const midY = rect.top + rect.height / 2;
    if (e.clientY < midY) {
      list.insertBefore(dragged, target);
    } else {
      list.insertBefore(dragged, target.nextSibling);
    }
  });

  // Нужно добавить draggable="true" в HTML
  list.querySelectorAll('li').forEach(li => {
    li.setAttribute('draggable', 'true');
  });
}

makeSortable(document.querySelector('.todo-list'));

Pointer Events API (рекомендуется для тач-устройств)

function makeDraggablePointer(el) {
  let startX, startY, startLeft, startTop;

  el.style.position = 'absolute';
  el.style.touchAction = 'none'; // отключить scroll

  el.addEventListener('pointerdown', (e) => {
    startX = e.clientX;
    startY = e.clientY;
    startLeft = el.offsetLeft;
    startTop  = el.offsetTop;
    el.setPointerCapture(e.pointerId); // события приходят на элемент
  });

  el.addEventListener('pointermove', (e) => {
    if (!e.buttons) return;
    el.style.left = `${startLeft + e.clientX - startX}px`;
    el.style.top  = `${startTop  + e.clientY - startY}px`;
  });
}

Частые ошибки

  • Не слушать события на documentmousemove нужно слушать на document, а не на элементе; если мышь выйдет за пределы, mousemove перестанет приходить.
  • Не вызывать preventDefault в dragover — без этого браузер не разрешит drop, и событие drop не сработает.
  • Игнорировать мобильные устройства — мышиные события не работают на тач; используйте Pointer Events или добавляйте поддержку touch-событий.

Связанные темы

Ресурсы