> Как реализовать перетаскивание модального окна мышкой на чистом JavaScript с использованием событий? (JavaScript)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: Cyber Interactive

Стек: JavaScript

> Пример ответа

Короткий ответ

Реализация перетаскивания модального окна на чистом JavaScript строится на обработке трёх событий мыши: mousedown на заголовке окна (начало перетаскивания), mousemove на document (обновление позиции) и mouseup на document (завершение). В mousedown запоминаем смещение курсора относительно окна, в mousemove вычисляем новую позицию через clientX/clientY минус смещение, в mouseup отписываемся от событий. Важно предотвращать выделение текста (user-select: none) и учитывать границы viewport.

Подробное объяснение

Ключевая идея - преобразование координат курсора в координаты окна через запоминание точки привязки. При mousedown на draggable-элементе (обычно заголовок) вычисляем offsetX = event.clientX - modal.offsetLeft и offsetY = event.clientY - modal.offsetTop. Затем на mousemove устанавливаем modal.style.left = event.clientX - offsetX + 'px' и top аналогично. Для предотвращения выхода за границы экрана добавляем проверки: Math.max(0, Math.min(maxX, newLeft)). Также нужно отключать стандартное поведение браузера (выделение, перетаскивание изображений) через event.preventDefault() и user-select: none на время перетаскивания.

На практике

Основные сценарии использования: кастомные диалоговые окна, панели инструментов, виджеты с произвольным позиционированием. Критично обрабатывать случай, когда пользователь быстро двигает мышь - события mousemove должны быть привязаны к document, а не к самому окну, иначе при выходе курсора за границы окна перетаскивание прервётся. Также нужно учитывать touch-события для мобильных устройств (аналогичная логика на touchstart/touchmove/touchend). Для производительности стоит использовать requestAnimationFrame при обновлении позиции, особенно если окно содержит тяжёлый контент.

Пример кода

JAVASCRIPT
class DraggableModal {
constructor(modalElement, handleElement) {
this.modal = modalElement;
this.handle = handleElement;
this.isDragging = false;
this.offset = { x: 0, y: 0 };
this.handle.addEventListener('mousedown', this.onMouseDown.bind(this));
}
onMouseDown(e) {
if (e.target.closest('.modal-header')) {
this.isDragging = true;
this.offset.x = e.clientX - this.modal.offsetLeft;
this.offset.y = e.clientY - this.modal.offsetTop;
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.onMouseUp);
document.body.style.userSelect = 'none';
e.preventDefault();
}
}
onMouseMove = (e) => {
if (!this.isDragging) return;
const newLeft = e.clientX - this.offset.x;
const newTop = e.clientY - this.offset.y;
const maxLeft = window.innerWidth - this.modal.offsetWidth;
const maxTop = window.innerHeight - this.modal.offsetHeight;
this.modal.style.left = Math.max(0, Math.min(newLeft, maxLeft)) + 'px';
this.modal.style.top = Math.max(0, Math.min(newTop, maxTop)) + 'px';
}
onMouseUp = () => {
this.isDragging = false;
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
document.body.style.userSelect = '';
}
}

Как отвечать на собеседовании

Начни с ключевой тройки событий и объяснения расчёта смещения. Затем добавь про граничные условия: привязка к document, предотвращение выделения, ограничение границами экрана. Упомяни touch-адаптацию и оптимизацию через requestAnimationFrame. Если спросят про альтернативы - можно упомянуть CSS resize или библиотеки, но подчеркни, что нативный JS даёт полный контроль. Хорошо показать понимание memory leaks (отписка от событий в mouseup).

Что проверяет интервьюер

  • Понимание event propagation и разницы между событиями на элементе и document
  • Умение работать с координатами и DOM-геометрией (offsetLeft, clientX)
  • Навыки обработки граничных случаев (быстрое движение, выход за экран)
  • Знание паттернов управления состоянием (флаг isDragging)
  • Понимание производительности (лишние reflow, debounce/throttle)

Типичные ошибки

  • Привязка mousemove к самому модальному окну вместо document - перетаскивание прерывается при выходе курсора
  • Забывают отписываться от событий в mouseup - утечка памяти и баги
  • Не учитывают начальное смещение курсора - окно "перескакивает" в угол
  • Игнорируют user-select: none - выделяется текст при перетаскивании
  • Не ограничивают позицию границами viewport - окно уходит за экран
  • Используют getBoundingClientRect вместо offsetLeft в mousedown - ошибка при CSS-трансформациях

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью