Drag&Drop
dataTransfer- объект классаDataTransfer, который нужен чтоб хранить перетаскиваемые данные. Сквозное хранилищеdataTransfer.effectAllowed- поддерживаемые эффекты (copy,move, ...) Drag'n'Drop. Навешивается на источнике вdragstartdataTransfer.dropEffect- используемый эффект (copy,move, ...) Drag'n'Drop. Навешивается на цели вdragover. Меняет вид указателя
Механизм
- На перетаскиваемый элемент надо добавит атрибут
draggable="true" - На перетаскиваемый элемент навесить
dragstart, прописать данные вDataTransfer - На те ноды, куда можно бросить, навесить
dragover, вызватьevent.preventDefault()чтобdropначал срабатывать - На ноду, на которую можно бросить (target), навесить событие
drop. В нём прописать логику успешности перетаскивания
Моменты
draggable
Не обязательно навешивать draggable="true" на весь элемент перетаскивания.
Навешивание draggable отключит возможность выделять текст в элементе.
Например, если навесить draggable="true" на строку tr, то пропадёт возможность выделять текст во всех ячейках этой строки.
Лучше добавить дополнительную ячейку, которую пометить как перемещаемую, а в качестве перетаскиваемого изображения указать родительский tr
Пример
<table>
<tr>
<td draggable="true" class="draggable-cell">☰</td>
<td>Содержимое ячейки</td>
</tr>
</table>
<script>
const dragStartHandler = event => {
// установка изображения, которое показывается при перетаскивании
const image = event.target.closest('tr');
event.dataTransfer.setDragImage(image, 0, 0);
}
document.querySelectorAll('.draggable-cell')
.forEach(item => item.addEventListener('dragstart', dragStartHandler));
</script>
dragend
Событие срабатывает в момент окончания перетаскивания.
Причем неважно, успешно перетащили или нет.
Если например в dragstart перетаскиваемый элемент положили в отдельную переменную, то в dragend эту переменную можно очистить.
dragover
Обработчик dragover по дефолту отменяет срабатывание drop.
Если в dragover не вызвать event.preventDefault(), событие drop не сработает.
Пример
drop
Обработчик drop — это логика, которая происходит в тот момент, когда перетаскиваемый элемент успешно отпущен.
Сюда можно прописать логику перемещения данных из одного массива в другой, или логику пересортировки массива.
Примеры
Перемещение данных из одного массива в другой на Vue2
<body>
<style>
.main {
width: 800px;
margin: 0 auto;
}
.source {
border: 1px solid coral;
padding: 20px 5px;
}
.target {
border: 1px solid darkorange;
margin-top: 40px;
padding: 20px 0;
}
.car__item {
border: 1px solid tomato;
padding: 15px;
margin: 5px;
box-sizing: border-box;
}
</style>
<div class="main">
<div id="app">
<div class="source">
<div
v-for="car in cars"
:key="'car_' + car.id"
@dragstart="dragStartHandler($event, car)"
draggable="true"
class="car__item"
>
{{ car.id }}: {{ car.name }} ({{ car.city }})
</div>
</div>
<div
@dragover="dragOverHandler"
@drop="dropHandler"
class="target"
>
<div v-for="car in movedCars" :key="'moved_car_' + car.id" class="car__item">
{{ car.id }}: {{ car.title }} ({{ car.city }})
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data () {
return {
cars: [
{ id: 1, title: 'Lada', city: 'Russia' },
{ id: 2, title: 'Skoda', city: 'Czech' },
{ id: 3, title: 'Audi', city: 'Germany' },
{ id: 4, title: 'Toyota', city: 'Japan' },
],
movedCars: [],
}
},
methods: {
dragStartHandler (event, car) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('carId', car.id);
},
dragOverHandler (event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
},
dropHandler (event) {
event.preventDefault();
const carId = event.dataTransfer.getData('carId');
this.switchCar(carId);
},
switchCar (carId) {
const carIndex = this.cars.findIndex(item => item.id == carId);
const car = this.cars.splice(carIndex, 1)[0];
this.movedCars.push(car);
}
},
});
</script>
</body>
Пересортировка списка
Полезная статья на htmlacademy
Основная идея — каждый элемент списка пометить как возможный для drop.
В событии drop смотрим, на какой элемент бросили. Получая его индекс — получим место в массиве, куда надо переместить запись.
Если хотим перемещать не только после, но и перед, то надо получить координаты, смотреть, в нижней или верхней части элемента сработал drop.
<body>
<style>
.main {
width: 800px;
margin: 0 auto;
}
.source {
border: 1px solid coral;
padding: 20px 5px;
}
.car__item {
border: 1px solid tomato;
padding: 15px;
margin: 5px;
box-sizing: border-box;
}
</style>
<div class="main">
<div id="app">
<div class="source">
<div
v-for="car in cars"
:key="'car_' + car.id"
@dragover="dragOverHandler($event, car)"
@drop="dropHandler($event, car)"
class="car__item"
>
<span
draggable="true"
@dragstart="dragStartHandler($event, car)"
@dragend="dragEndHandler"
>☰</span>
{{ car.id }}: {{ car.title }} ({{ car.city }})
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data () {
return {
cars: [
{ id: 1, title: 'Lada', city: 'Russia' },
{ id: 2, title: 'Skoda', city: 'Czech' },
{ id: 3, title: 'Audi', city: 'Germany' },
{ id: 4, title: 'Toyota', city: 'Japan' },
],
draggableCar: null,
}
},
methods: {
dragStartHandler (event, car) {
this.draggableCar = car;
const image = event.target.closest('div');
event.dataTransfer.setDragImage(image, 0, 0);
event.dataTransfer.effectAllowed = 'move';
},
dragEndHandler () {
this.draggableCar = null;
},
dragOverHandler (event, car) {
if (car === this.draggableCar) {
// бросать на себя не имеет смысла
return;
}
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
},
dropHandler (event, car) {
event.preventDefault();
// приземлять перед или после элемента в зависимости от того, на какой части элемента отпустили
const domRect = event.target.getBoundingClientRect();
const offset = event.clientY < (domRect.y + (domRect.height / 2)) ? 0 : 1;
this.cars.splice(this.cars.indexOf(this.draggableCar), 1);
const targetIndex = this.cars.indexOf(car);
this.cars.splice(targetIndex + offset, 0, this.draggableCar);
},
},
});
</script>
</body>