Вы разрабатываете Kanban-доску: карточки нужно перетаскивать между колонками. Или графический редактор: рисование по холсту. Обе задачи строятся на трёх событиях: mousedown → mousemove → mouseup. Понять события мыши — значит уметь строить интерактивные интерфейсы.
Браузер генерирует события мыши при любом взаимодействии с указателем. Объект MouseEvent содержит координаты, нажатую кнопку и удерживаемые модификаторы.
element.addEventListener('mousedown', handler) // кнопка нажата
element.addEventListener('mouseup', handler) // кнопка отпущена
element.addEventListener('click', handler) // полный клик (down + up)
element.addEventListener('dblclick', handler) // двойной клик
element.addEventListener('contextmenu', handler) // правая кнопка мышиelement.addEventListener('mousemove', handler) // мышь двигается над элементом
element.addEventListener('mouseover', handler) // мышь вошла в элемент или его потомка
element.addEventListener('mouseout', handler) // мышь вышла из элемента или его потомка
element.addEventListener('mouseenter', handler) // мышь вошла в элемент (без всплытия)
element.addEventListener('mouseleave', handler) // мышь вышла из элемента (без всплытия)document.addEventListener('click', (event) => {
// Какая кнопка нажата
console.log(event.button) // 0 — левая, 1 — средняя, 2 — правая
console.log(event.buttons) // битовая маска: 1=левая, 2=правая, 4=средняя
// Координаты клика
console.log(event.clientX, event.clientY) // относительно viewport (видимой области)
console.log(event.pageX, event.pageY) // относительно всей страницы (с учётом прокрутки)
console.log(event.offsetX, event.offsetY) // относительно элемента-цели
// Модификаторы
console.log(event.ctrlKey) // удержан Ctrl
console.log(event.shiftKey) // удержан Shift
console.log(event.altKey) // удержан Alt
})Ключевое различие — всплытие (bubbling):
| Событие | Всплывает | Срабатывает при входе в потомка |
|---|---|---|
| mouseover | Да | Да |
| mouseout | Да | Да |
| mouseenter | Нет | Нет |
| mouseleave | Нет | Нет |
// mouseover срабатывает при каждом переходе между дочерними элементами
// mouseenter — только один раз при входе в родительский элемент
// Для hover-эффектов предпочитай mouseenter/mouseleave// clientX/Y — координаты от левого верхнего угла ВИДИМОЙ области
// Не меняются при прокрутке страницы
console.log(event.clientX) // позиция в пикселях от левого края viewport
// pageX/Y — координаты от левого верхнего угла ДОКУМЕНТА
// Учитывают вертикальную и горизонтальную прокрутку
console.log(event.pageX) // clientX + window.scrollX
// offsetX/Y — координаты относительно ЦЕЛЕВОГО ЭЛЕМЕНТА
// Удобно для рисования на canvas или drag'n'drop
console.log(event.offsetX) // позиция от левого края элемента// Отключить контекстное меню (например, для кастомного меню)
element.addEventListener('contextmenu', (event) => {
event.preventDefault()
showCustomContextMenu(event.clientX, event.clientY)
})
// Запретить выделение текста при drag'n'drop
element.addEventListener('mousedown', (event) => {
event.preventDefault()
})let isDragging = false
let startX = 0, startY = 0
let currentX = 0, currentY = 0
draggable.addEventListener('mousedown', (event) => {
isDragging = true
startX = event.clientX - currentX
startY = event.clientY - currentY
event.preventDefault() // запретить выделение
})
document.addEventListener('mousemove', (event) => {
if (!isDragging) return
currentX = event.clientX - startX
currentY = event.clientY - startY
draggable.style.transform = `translate(${currentX}px, ${currentY}px)`
})
document.addEventListener('mouseup', () => {
isDragging = false
})Обработчики mousemove и mouseup вешаются на document, а не на элемент — иначе при быстром движении мыши курсор «выскользнет» из элемента и перетаскивание прервётся.
Ошибка 1: mousemove и mouseup на элементе, а не на document
// Сломано: при быстром движении мышь выходит за пределы элемента
draggable.addEventListener('mousemove', handler) // пропустит события!
draggable.addEventListener('mouseup', handler)
// Исправлено: глобальные обработчики — мышь перехватывается везде
document.addEventListener('mousemove', handler)
document.addEventListener('mouseup', handler)Ошибка 2: не вызывают preventDefault при перетаскивании
// Сломано: при перетаскивании браузер выделяет текст, появляется "призрак"
draggable.addEventListener('mousedown', (e) => {
isDragging = true // не вызвали e.preventDefault()
})
// Исправлено:
draggable.addEventListener('mousedown', (e) => {
e.preventDefault() // запретить выделение текста
isDragging = true
})Ошибка 3: используют mouseover вместо mouseenter для hover
// Сломано: mouseover срабатывает при каждом переходе между дочерними элементами
list.addEventListener('mouseover', () => list.classList.add('hovered'))
// Мигает при наведении на пункты списка!
// Исправлено: mouseenter срабатывает только при входе в родительский элемент
list.addEventListener('mouseenter', () => list.classList.add('hovered'))
list.addEventListener('mouseleave', () => list.classList.remove('hovered'))Симуляция drag'n'drop логики через mousedown/mousemove/mouseup с отслеживанием позиции
// Симуляция Drag'n'Drop через чистую логику (без DOM)
// Воспроизводим паттерн: mousedown → mousemove × N → mouseup
function createDragSession(startX, startY) {
let currentX = startX
let currentY = startY
let isDragging = true
const BOUNDS = { minX: 0, minY: 0, maxX: 800, maxY: 600 }
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value))
}
return {
// Применить дельту движения (как событие mousemove)
move(deltaX, deltaY) {
if (!isDragging) return null
currentX = clamp(currentX + deltaX, BOUNDS.minX, BOUNDS.maxX)
currentY = clamp(currentY + deltaY, BOUNDS.minY, BOUNDS.maxY)
return { x: currentX, y: currentY }
},
// Завершить перетаскивание (как событие mouseup)
drop() {
isDragging = false
return { x: currentX, y: currentY }
},
getPosition() {
return { x: currentX, y: currentY, isDragging }
}
}
}
// Симулируем перетаскивание элемента с (100, 150) к (350, 280)
console.log('=== Симуляция Drag'n'Drop ===')
const drag = createDragSession(100, 150)
console.log('mousedown:', drag.getPosition())
// { x: 100, y: 150, isDragging: true }
// Серия событий mousemove с дельтами
const moves = [
{ dx: 50, dy: 30 },
{ dx: 80, dy: 50 },
{ dx: 70, dy: 30 },
{ dx: 50, dy: 20 },
]
moves.forEach((move, i) => {
const pos = drag.move(move.dx, move.dy)
console.log(`mousemove[${i + 1}]: x=${pos.x}, y=${pos.y}`)
})
// mouseup — завершаем перетаскивание
const finalPos = drag.drop()
console.log('mouseup (final):', finalPos)
// { x: 350, y: 280 }
// Попытка продолжить после drop — игнорируется
const afterDrop = drag.move(100, 100)
console.log('Движение после drop:', afterDrop) // null
// Проверяем clamping к границам
console.log('\n=== Проверка ограничений (clamping) ===')
const drag2 = createDragSession(750, 550)
const clamped = drag2.move(200, 200) // выходит за границы 800x600
console.log('Перемещение за границы:', clamped)
// { x: 800, y: 600 } — зафиксировано на границе
// Реальный паттерн обработчиков в браузере
console.log('\n=== Паттерн обработчиков ===')
const mockEvents = {
mousedown: { clientX: 200, clientY: 300, button: 0 },
mousemove: [
{ clientX: 220, clientY: 315 },
{ clientX: 240, clientY: 330 },
],
mouseup: { clientX: 250, clientY: 340 },
}
let startClientX = 0, startClientY = 0, offsetX = 0, offsetY = 0
let dragging = false
// Симуляция mousedown
const e = mockEvents.mousedown
if (e.button === 0) { // только левая кнопка
dragging = true
startClientX = e.clientX
startClientY = e.clientY
console.log('mousedown: начало перетаскивания', { startClientX, startClientY })
}
// Симуляция mousemove
mockEvents.mousemove.forEach(ev => {
if (!dragging) return
offsetX = ev.clientX - startClientX
offsetY = ev.clientY - startClientY
console.log(`mousemove: смещение dx=${offsetX}, dy=${offsetY}`)
})
// Симуляция mouseup
if (dragging) {
dragging = false
console.log('mouseup: перетаскивание завершено, итоговое смещение', { offsetX, offsetY })
}Вы разрабатываете Kanban-доску: карточки нужно перетаскивать между колонками. Или графический редактор: рисование по холсту. Обе задачи строятся на трёх событиях: mousedown → mousemove → mouseup. Понять события мыши — значит уметь строить интерактивные интерфейсы.
Браузер генерирует события мыши при любом взаимодействии с указателем. Объект MouseEvent содержит координаты, нажатую кнопку и удерживаемые модификаторы.
element.addEventListener('mousedown', handler) // кнопка нажата
element.addEventListener('mouseup', handler) // кнопка отпущена
element.addEventListener('click', handler) // полный клик (down + up)
element.addEventListener('dblclick', handler) // двойной клик
element.addEventListener('contextmenu', handler) // правая кнопка мышиelement.addEventListener('mousemove', handler) // мышь двигается над элементом
element.addEventListener('mouseover', handler) // мышь вошла в элемент или его потомка
element.addEventListener('mouseout', handler) // мышь вышла из элемента или его потомка
element.addEventListener('mouseenter', handler) // мышь вошла в элемент (без всплытия)
element.addEventListener('mouseleave', handler) // мышь вышла из элемента (без всплытия)document.addEventListener('click', (event) => {
// Какая кнопка нажата
console.log(event.button) // 0 — левая, 1 — средняя, 2 — правая
console.log(event.buttons) // битовая маска: 1=левая, 2=правая, 4=средняя
// Координаты клика
console.log(event.clientX, event.clientY) // относительно viewport (видимой области)
console.log(event.pageX, event.pageY) // относительно всей страницы (с учётом прокрутки)
console.log(event.offsetX, event.offsetY) // относительно элемента-цели
// Модификаторы
console.log(event.ctrlKey) // удержан Ctrl
console.log(event.shiftKey) // удержан Shift
console.log(event.altKey) // удержан Alt
})Ключевое различие — всплытие (bubbling):
| Событие | Всплывает | Срабатывает при входе в потомка |
|---|---|---|
| mouseover | Да | Да |
| mouseout | Да | Да |
| mouseenter | Нет | Нет |
| mouseleave | Нет | Нет |
// mouseover срабатывает при каждом переходе между дочерними элементами
// mouseenter — только один раз при входе в родительский элемент
// Для hover-эффектов предпочитай mouseenter/mouseleave// clientX/Y — координаты от левого верхнего угла ВИДИМОЙ области
// Не меняются при прокрутке страницы
console.log(event.clientX) // позиция в пикселях от левого края viewport
// pageX/Y — координаты от левого верхнего угла ДОКУМЕНТА
// Учитывают вертикальную и горизонтальную прокрутку
console.log(event.pageX) // clientX + window.scrollX
// offsetX/Y — координаты относительно ЦЕЛЕВОГО ЭЛЕМЕНТА
// Удобно для рисования на canvas или drag'n'drop
console.log(event.offsetX) // позиция от левого края элемента// Отключить контекстное меню (например, для кастомного меню)
element.addEventListener('contextmenu', (event) => {
event.preventDefault()
showCustomContextMenu(event.clientX, event.clientY)
})
// Запретить выделение текста при drag'n'drop
element.addEventListener('mousedown', (event) => {
event.preventDefault()
})let isDragging = false
let startX = 0, startY = 0
let currentX = 0, currentY = 0
draggable.addEventListener('mousedown', (event) => {
isDragging = true
startX = event.clientX - currentX
startY = event.clientY - currentY
event.preventDefault() // запретить выделение
})
document.addEventListener('mousemove', (event) => {
if (!isDragging) return
currentX = event.clientX - startX
currentY = event.clientY - startY
draggable.style.transform = `translate(${currentX}px, ${currentY}px)`
})
document.addEventListener('mouseup', () => {
isDragging = false
})Обработчики mousemove и mouseup вешаются на document, а не на элемент — иначе при быстром движении мыши курсор «выскользнет» из элемента и перетаскивание прервётся.
Ошибка 1: mousemove и mouseup на элементе, а не на document
// Сломано: при быстром движении мышь выходит за пределы элемента
draggable.addEventListener('mousemove', handler) // пропустит события!
draggable.addEventListener('mouseup', handler)
// Исправлено: глобальные обработчики — мышь перехватывается везде
document.addEventListener('mousemove', handler)
document.addEventListener('mouseup', handler)Ошибка 2: не вызывают preventDefault при перетаскивании
// Сломано: при перетаскивании браузер выделяет текст, появляется "призрак"
draggable.addEventListener('mousedown', (e) => {
isDragging = true // не вызвали e.preventDefault()
})
// Исправлено:
draggable.addEventListener('mousedown', (e) => {
e.preventDefault() // запретить выделение текста
isDragging = true
})Ошибка 3: используют mouseover вместо mouseenter для hover
// Сломано: mouseover срабатывает при каждом переходе между дочерними элементами
list.addEventListener('mouseover', () => list.classList.add('hovered'))
// Мигает при наведении на пункты списка!
// Исправлено: mouseenter срабатывает только при входе в родительский элемент
list.addEventListener('mouseenter', () => list.classList.add('hovered'))
list.addEventListener('mouseleave', () => list.classList.remove('hovered'))Симуляция drag'n'drop логики через mousedown/mousemove/mouseup с отслеживанием позиции
// Симуляция Drag'n'Drop через чистую логику (без DOM)
// Воспроизводим паттерн: mousedown → mousemove × N → mouseup
function createDragSession(startX, startY) {
let currentX = startX
let currentY = startY
let isDragging = true
const BOUNDS = { minX: 0, minY: 0, maxX: 800, maxY: 600 }
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value))
}
return {
// Применить дельту движения (как событие mousemove)
move(deltaX, deltaY) {
if (!isDragging) return null
currentX = clamp(currentX + deltaX, BOUNDS.minX, BOUNDS.maxX)
currentY = clamp(currentY + deltaY, BOUNDS.minY, BOUNDS.maxY)
return { x: currentX, y: currentY }
},
// Завершить перетаскивание (как событие mouseup)
drop() {
isDragging = false
return { x: currentX, y: currentY }
},
getPosition() {
return { x: currentX, y: currentY, isDragging }
}
}
}
// Симулируем перетаскивание элемента с (100, 150) к (350, 280)
console.log('=== Симуляция Drag'n'Drop ===')
const drag = createDragSession(100, 150)
console.log('mousedown:', drag.getPosition())
// { x: 100, y: 150, isDragging: true }
// Серия событий mousemove с дельтами
const moves = [
{ dx: 50, dy: 30 },
{ dx: 80, dy: 50 },
{ dx: 70, dy: 30 },
{ dx: 50, dy: 20 },
]
moves.forEach((move, i) => {
const pos = drag.move(move.dx, move.dy)
console.log(`mousemove[${i + 1}]: x=${pos.x}, y=${pos.y}`)
})
// mouseup — завершаем перетаскивание
const finalPos = drag.drop()
console.log('mouseup (final):', finalPos)
// { x: 350, y: 280 }
// Попытка продолжить после drop — игнорируется
const afterDrop = drag.move(100, 100)
console.log('Движение после drop:', afterDrop) // null
// Проверяем clamping к границам
console.log('\n=== Проверка ограничений (clamping) ===')
const drag2 = createDragSession(750, 550)
const clamped = drag2.move(200, 200) // выходит за границы 800x600
console.log('Перемещение за границы:', clamped)
// { x: 800, y: 600 } — зафиксировано на границе
// Реальный паттерн обработчиков в браузере
console.log('\n=== Паттерн обработчиков ===')
const mockEvents = {
mousedown: { clientX: 200, clientY: 300, button: 0 },
mousemove: [
{ clientX: 220, clientY: 315 },
{ clientX: 240, clientY: 330 },
],
mouseup: { clientX: 250, clientY: 340 },
}
let startClientX = 0, startClientY = 0, offsetX = 0, offsetY = 0
let dragging = false
// Симуляция mousedown
const e = mockEvents.mousedown
if (e.button === 0) { // только левая кнопка
dragging = true
startClientX = e.clientX
startClientY = e.clientY
console.log('mousedown: начало перетаскивания', { startClientX, startClientY })
}
// Симуляция mousemove
mockEvents.mousemove.forEach(ev => {
if (!dragging) return
offsetX = ev.clientX - startClientX
offsetY = ev.clientY - startClientY
console.log(`mousemove: смещение dx=${offsetX}, dy=${offsetY}`)
})
// Симуляция mouseup
if (dragging) {
dragging = false
console.log('mouseup: перетаскивание завершено, итоговое смещение', { offsetX, offsetY })
}Напиши функцию dragAndDrop(startX, startY, moves, endX, endY), которая симулирует перетаскивание. Параметр moves — массив объектов { dx, dy }. Функция должна начать с позиции startX/startY, применить каждое смещение, ограничить результат границами [0, endX] по X и [0, endY] по Y, и вернуть итоговую позицию { x, y, path } где path — массив всех промежуточных позиций.
x = clamp(x + move.dx, 0, maxX), y = clamp(y + move.dy, 0, maxY). Начни с path = [{ x: startX, y: startY }] и добавляй новую точку после каждого хода.