Представь: ты строишь Kanban-доску для управления задачами. Пользователь должен перетаскивать карточки между колонками «К выполнению», «В работе», «Готово». Браузер предоставляет встроенный API для этого — без сторонних библиотек, без сложных хаков.
Drag'n'Drop API позволяет передавать данные от перетаскиваемого элемента к зоне сброса через dataTransfer. Без него пришлось бы вручную отслеживать mousedown/mousemove/mouseup и вычислять позиции — десятки строк кода вместо нескольких обработчиков событий.
event.preventDefault() используется так жеЧтобы сделать элемент перетаскиваемым, нужно добавить атрибут draggable="true":
// HTML: <div draggable="true" id="card">Задача</div>
const card = document.getElementById('card')
// Теперь элемент можно перетаскиватьelement.addEventListener('dragstart', (event) => {
// Срабатывает в начале перетаскивания
// Здесь записываем данные, которые хотим передать
event.dataTransfer.setData('text/plain', element.id)
event.dataTransfer.effectAllowed = 'move'
element.classList.add('dragging')
})
element.addEventListener('drag', (event) => {
// Срабатывает непрерывно во время перетаскивания
// Редко используется напрямую
})
element.addEventListener('dragend', (event) => {
// Срабатывает когда отпустили кнопку мыши
element.classList.remove('dragging')
})dropZone.addEventListener('dragover', (event) => {
// ВАЖНО: preventDefault() разрешает сброс в эту зону
// Без него drop не сработает!
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
dropZone.classList.add('drag-over')
})
dropZone.addEventListener('dragleave', (event) => {
dropZone.classList.remove('drag-over')
})
dropZone.addEventListener('drop', (event) => {
event.preventDefault()
// Получаем переданные данные
const id = event.dataTransfer.getData('text/plain')
const draggedElement = document.getElementById(id)
dropZone.appendChild(draggedElement)
dropZone.classList.remove('drag-over')
})Объект event.dataTransfer — канал передачи данных между источником и целью:
// На dragstart — записываем
event.dataTransfer.setData('text/plain', 'card-id-42')
event.dataTransfer.setData('application/json', JSON.stringify({ id: 42, title: 'Задача' }))
// На drop — читаем
const id = event.dataTransfer.getData('text/plain')
const data = JSON.parse(event.dataTransfer.getData('application/json'))
// Типы эффектов
event.dataTransfer.effectAllowed = 'move' // только перемещение
event.dataTransfer.effectAllowed = 'copy' // только копирование
event.dataTransfer.effectAllowed = 'copyMove' // оба вариантаКлассический Kanban — перетаскивание карточек между колонками: "К выполнению" → "В работе" → "Готово".
// Каждая карточка сохраняет свой id при dragstart
// Каждая колонка принимает drop и переносит карточку
// dragover с preventDefault позволяет сбросить
card.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', card.dataset.id)
})
column.addEventListener('dragover', e => e.preventDefault())
column.addEventListener('drop', e => {
e.preventDefault()
const cardId = e.dataTransfer.getData('text/plain')
const card = document.querySelector(`[data-id="${cardId}"]`)
column.querySelector('.cards').appendChild(card)
})Браузер по умолчанию запрещает сброс на большинство элементов — чтобы не нарушать стандартное поведение страницы. Вызов event.preventDefault() в обработчике dragover — это явный сигнал браузеру: «эта зона принимает перетаскиваемые элементы».
1. Не вызвать preventDefault в dragover — drop не сработает
// ПЛОХО — без preventDefault событие drop никогда не придёт
dropZone.addEventListener('dragover', (event) => {
// event.preventDefault() забыли!
dropZone.classList.add('over')
})
dropZone.addEventListener('drop', (event) => {
// Этот обработчик никогда не вызовется
})
// ХОРОШО — preventDefault разрешает сброс
dropZone.addEventListener('dragover', (event) => {
event.preventDefault() // обязательно!
dropZone.classList.add('over')
})2. Использовать event.target в drop вместо данных из dataTransfer
// ПЛОХО — event.target при drop это dropZone, а не перетаскиваемый элемент
dropZone.addEventListener('drop', (event) => {
event.preventDefault()
const dragged = event.target // НЕПРАВИЛЬНО — это dropZone!
dropZone.appendChild(dragged)
})
// ХОРОШО — данные передаются через dataTransfer
dropZone.addEventListener('drop', (event) => {
event.preventDefault()
const id = event.dataTransfer.getData('text/plain')
const dragged = document.getElementById(id) // находим по id
dropZone.appendChild(dragged)
})3. Не очищать стили drag-over при dragleave
// ПЛОХО — подсветка зоны остаётся после ухода курсора
dropZone.addEventListener('dragover', (event) => {
event.preventDefault()
dropZone.style.border = '2px dashed blue'
})
// dragleave не обработан — рамка висит навсегда
// ХОРОШО — убирать стили в dragleave
dropZone.addEventListener('dragleave', () => {
dropZone.style.border = ''
})
dropZone.addEventListener('drop', (event) => {
event.preventDefault()
dropZone.style.border = '' // тоже убираем при сбросе
})event.dataTransfer.files)Симуляция Kanban-доски: перетаскивание карточек между колонками через mock-объекты (без DOM)
// Симуляция Kanban-доски с логикой Drag'n'Drop
// В браузере это были бы реальные DOM-элементы с событиями
// Структура данных доски
const board = {
todo: { title: 'К выполнению', cards: ['Верстка главной', 'API авторизации', 'Тесты'] },
inProgress: { title: 'В работе', cards: ['Корзина товаров'] },
done: { title: 'Готово', cards: ['Дизайн макета'] },
}
function printBoard(board) {
for (const [key, col] of Object.entries(board)) {
console.log(`[${col.title}]: ${col.cards.length ? col.cards.join(', ') : '(пусто)'}`)
}
}
// --- Mock dataTransfer ---
function createDataTransfer() {
const store = {}
return {
setData(type, value) { store[type] = value },
getData(type) { return store[type] || '' },
effectAllowed: 'move',
dropEffect: 'move',
}
}
// --- Симуляция событий dragstart / dragover / drop ---
function simulateDrag(board, fromCol, cardTitle, toCol) {
// dragstart: запоминаем данные карточки
const dataTransfer = createDataTransfer()
const dragStartEvent = { dataTransfer, preventDefault: () => {} }
dataTransfer.setData('text/plain', cardTitle)
dataTransfer.setData('application/json', JSON.stringify({ card: cardTitle, from: fromCol }))
console.log(`dragstart: начали тащить "${cardTitle}" из "${board[fromCol].title}"`)
// dragover: preventDefault разрешает drop
const dragOverEvent = { preventDefault: () => {}, dataTransfer }
dragOverEvent.preventDefault()
console.log(`dragover: зона "${board[toCol].title}" приняла перетаскивание`)
// drop: перемещаем карточку
const dropEvent = { dataTransfer, preventDefault: () => {} }
dropEvent.preventDefault()
const droppedCard = dropEvent.dataTransfer.getData('text/plain')
const meta = JSON.parse(dropEvent.dataTransfer.getData('application/json'))
// Убираем из исходной колонки
board[meta.from].cards = board[meta.from].cards.filter(c => c !== droppedCard)
// Добавляем в целевую колонку
board[toCol].cards.push(droppedCard)
console.log(`drop: "${droppedCard}" перемещена в "${board[toCol].title}"`)
}
console.log('=== Начальное состояние доски ===')
printBoard(board)
console.log('\n=== Перетаскиваем "API авторизации" → В работе ===')
simulateDrag(board, 'todo', 'API авторизации', 'inProgress')
console.log('\nСостояние после:')
printBoard(board)
console.log('\n=== Перетаскиваем "Корзина товаров" → Готово ===')
simulateDrag(board, 'inProgress', 'Корзина товаров', 'done')
console.log('\nФинальное состояние:')
printBoard(board)
// --- Проверка dropEffect ---
console.log('\n=== Типы эффектов dataTransfer ===')
const dt = createDataTransfer()
dt.effectAllowed = 'copyMove'
dt.setData('text/plain', 'test-data')
console.log('effectAllowed:', dt.effectAllowed)
console.log('getData:', dt.getData('text/plain'))Представь: ты строишь Kanban-доску для управления задачами. Пользователь должен перетаскивать карточки между колонками «К выполнению», «В работе», «Готово». Браузер предоставляет встроенный API для этого — без сторонних библиотек, без сложных хаков.
Drag'n'Drop API позволяет передавать данные от перетаскиваемого элемента к зоне сброса через dataTransfer. Без него пришлось бы вручную отслеживать mousedown/mousemove/mouseup и вычислять позиции — десятки строк кода вместо нескольких обработчиков событий.
event.preventDefault() используется так жеЧтобы сделать элемент перетаскиваемым, нужно добавить атрибут draggable="true":
// HTML: <div draggable="true" id="card">Задача</div>
const card = document.getElementById('card')
// Теперь элемент можно перетаскиватьelement.addEventListener('dragstart', (event) => {
// Срабатывает в начале перетаскивания
// Здесь записываем данные, которые хотим передать
event.dataTransfer.setData('text/plain', element.id)
event.dataTransfer.effectAllowed = 'move'
element.classList.add('dragging')
})
element.addEventListener('drag', (event) => {
// Срабатывает непрерывно во время перетаскивания
// Редко используется напрямую
})
element.addEventListener('dragend', (event) => {
// Срабатывает когда отпустили кнопку мыши
element.classList.remove('dragging')
})dropZone.addEventListener('dragover', (event) => {
// ВАЖНО: preventDefault() разрешает сброс в эту зону
// Без него drop не сработает!
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
dropZone.classList.add('drag-over')
})
dropZone.addEventListener('dragleave', (event) => {
dropZone.classList.remove('drag-over')
})
dropZone.addEventListener('drop', (event) => {
event.preventDefault()
// Получаем переданные данные
const id = event.dataTransfer.getData('text/plain')
const draggedElement = document.getElementById(id)
dropZone.appendChild(draggedElement)
dropZone.classList.remove('drag-over')
})Объект event.dataTransfer — канал передачи данных между источником и целью:
// На dragstart — записываем
event.dataTransfer.setData('text/plain', 'card-id-42')
event.dataTransfer.setData('application/json', JSON.stringify({ id: 42, title: 'Задача' }))
// На drop — читаем
const id = event.dataTransfer.getData('text/plain')
const data = JSON.parse(event.dataTransfer.getData('application/json'))
// Типы эффектов
event.dataTransfer.effectAllowed = 'move' // только перемещение
event.dataTransfer.effectAllowed = 'copy' // только копирование
event.dataTransfer.effectAllowed = 'copyMove' // оба вариантаКлассический Kanban — перетаскивание карточек между колонками: "К выполнению" → "В работе" → "Готово".
// Каждая карточка сохраняет свой id при dragstart
// Каждая колонка принимает drop и переносит карточку
// dragover с preventDefault позволяет сбросить
card.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', card.dataset.id)
})
column.addEventListener('dragover', e => e.preventDefault())
column.addEventListener('drop', e => {
e.preventDefault()
const cardId = e.dataTransfer.getData('text/plain')
const card = document.querySelector(`[data-id="${cardId}"]`)
column.querySelector('.cards').appendChild(card)
})Браузер по умолчанию запрещает сброс на большинство элементов — чтобы не нарушать стандартное поведение страницы. Вызов event.preventDefault() в обработчике dragover — это явный сигнал браузеру: «эта зона принимает перетаскиваемые элементы».
1. Не вызвать preventDefault в dragover — drop не сработает
// ПЛОХО — без preventDefault событие drop никогда не придёт
dropZone.addEventListener('dragover', (event) => {
// event.preventDefault() забыли!
dropZone.classList.add('over')
})
dropZone.addEventListener('drop', (event) => {
// Этот обработчик никогда не вызовется
})
// ХОРОШО — preventDefault разрешает сброс
dropZone.addEventListener('dragover', (event) => {
event.preventDefault() // обязательно!
dropZone.classList.add('over')
})2. Использовать event.target в drop вместо данных из dataTransfer
// ПЛОХО — event.target при drop это dropZone, а не перетаскиваемый элемент
dropZone.addEventListener('drop', (event) => {
event.preventDefault()
const dragged = event.target // НЕПРАВИЛЬНО — это dropZone!
dropZone.appendChild(dragged)
})
// ХОРОШО — данные передаются через dataTransfer
dropZone.addEventListener('drop', (event) => {
event.preventDefault()
const id = event.dataTransfer.getData('text/plain')
const dragged = document.getElementById(id) // находим по id
dropZone.appendChild(dragged)
})3. Не очищать стили drag-over при dragleave
// ПЛОХО — подсветка зоны остаётся после ухода курсора
dropZone.addEventListener('dragover', (event) => {
event.preventDefault()
dropZone.style.border = '2px dashed blue'
})
// dragleave не обработан — рамка висит навсегда
// ХОРОШО — убирать стили в dragleave
dropZone.addEventListener('dragleave', () => {
dropZone.style.border = ''
})
dropZone.addEventListener('drop', (event) => {
event.preventDefault()
dropZone.style.border = '' // тоже убираем при сбросе
})event.dataTransfer.files)Симуляция Kanban-доски: перетаскивание карточек между колонками через mock-объекты (без DOM)
// Симуляция Kanban-доски с логикой Drag'n'Drop
// В браузере это были бы реальные DOM-элементы с событиями
// Структура данных доски
const board = {
todo: { title: 'К выполнению', cards: ['Верстка главной', 'API авторизации', 'Тесты'] },
inProgress: { title: 'В работе', cards: ['Корзина товаров'] },
done: { title: 'Готово', cards: ['Дизайн макета'] },
}
function printBoard(board) {
for (const [key, col] of Object.entries(board)) {
console.log(`[${col.title}]: ${col.cards.length ? col.cards.join(', ') : '(пусто)'}`)
}
}
// --- Mock dataTransfer ---
function createDataTransfer() {
const store = {}
return {
setData(type, value) { store[type] = value },
getData(type) { return store[type] || '' },
effectAllowed: 'move',
dropEffect: 'move',
}
}
// --- Симуляция событий dragstart / dragover / drop ---
function simulateDrag(board, fromCol, cardTitle, toCol) {
// dragstart: запоминаем данные карточки
const dataTransfer = createDataTransfer()
const dragStartEvent = { dataTransfer, preventDefault: () => {} }
dataTransfer.setData('text/plain', cardTitle)
dataTransfer.setData('application/json', JSON.stringify({ card: cardTitle, from: fromCol }))
console.log(`dragstart: начали тащить "${cardTitle}" из "${board[fromCol].title}"`)
// dragover: preventDefault разрешает drop
const dragOverEvent = { preventDefault: () => {}, dataTransfer }
dragOverEvent.preventDefault()
console.log(`dragover: зона "${board[toCol].title}" приняла перетаскивание`)
// drop: перемещаем карточку
const dropEvent = { dataTransfer, preventDefault: () => {} }
dropEvent.preventDefault()
const droppedCard = dropEvent.dataTransfer.getData('text/plain')
const meta = JSON.parse(dropEvent.dataTransfer.getData('application/json'))
// Убираем из исходной колонки
board[meta.from].cards = board[meta.from].cards.filter(c => c !== droppedCard)
// Добавляем в целевую колонку
board[toCol].cards.push(droppedCard)
console.log(`drop: "${droppedCard}" перемещена в "${board[toCol].title}"`)
}
console.log('=== Начальное состояние доски ===')
printBoard(board)
console.log('\n=== Перетаскиваем "API авторизации" → В работе ===')
simulateDrag(board, 'todo', 'API авторизации', 'inProgress')
console.log('\nСостояние после:')
printBoard(board)
console.log('\n=== Перетаскиваем "Корзина товаров" → Готово ===')
simulateDrag(board, 'inProgress', 'Корзина товаров', 'done')
console.log('\nФинальное состояние:')
printBoard(board)
// --- Проверка dropEffect ---
console.log('\n=== Типы эффектов dataTransfer ===')
const dt = createDataTransfer()
dt.effectAllowed = 'copyMove'
dt.setData('text/plain', 'test-data')
console.log('effectAllowed:', dt.effectAllowed)
console.log('getData:', dt.getData('text/plain'))Реализуй функцию moveCard(board, cardId, fromColumn, toColumn), которая перемещает карточку между колонками Kanban-доски. Если карточка не найдена в исходной колонке — выбрось ошибку. Также реализуй getColumn(board, cardId), которая находит в какой колонке находится карточка.
indexOf(cardId), если === -1 то ошибка. splice(index, 1) удаляет элемент. push(cardId) добавляет. includes(cardId) в getColumn.