Чи можете ви уявити таблицю, у якій вам доводиться натискати кнопки "вгору" та "вниз" 10 тисяч разів, щоб перемістити рядок знизу вгору?
Я знаю сервіси/веб-додатки, які так роблять, і мені це дуже не подобається!
Отже!
У цій статті давайте розглянемо, як ми можемо створити таблицю, яка дозволяє змінювати порядок рядків за допомогою перетягування! З нуля!
Ми будемо використовувати React та Typescript, але схожі (не однакові, на жаль) ідеї застосовні і для звичайного HTML/JavaScript!
Налаштування
Проста таблиця Pokémon для відображення номеру індексу та імені.
import React from "react"
export type Pokemon = {
id: number
name: string
}
const initailPokemons: Pokemon[] = [
{ id: 1, name: "bulbasaur" },
{ id: 2, name: "ivysaur" },
{ id: 3, name: "venusaur" },
{ id: 4, name: "charmander" },
{ id: 5, name: "charmeleon" },
{ id: 6, name: "charizard" },
{ id: 7, name: "squirtle" },
{ id: 8, name: "blastoise" },
{ id: 9, name: "caterpie" },
{ id: 10, name: "metapod" },
]
export default function Demo() {
const [pokemons, setPokemons] = React.useState(initailPokemons)
return (
Pokemons
{pokemons.map((pokemon, index) => ( ))}
No. Name {pokemon.id} {pokemon.name}
); }
``` (Чому Пікачу — №25….Я лінувався вводити 25 записів, тому залишу це на сьогодні…)  Я використовую tailwind для стилізації, але, звісно, це ніяк не впливає на логіку перетягування та скидання!
## Перетягування
Як і в попередніх статтях, де я розповідав, як ми можемо [**_підтримувати перетягування та скидання для завантаження файлів_**](/gitconnected/html-javascript-support-drag-and-drop-for-file-upload-28d60d7f3461), ми знову будемо використовувати [**API перетягування та скидання**](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API), але з іншими подіями.
По-перше, нам потрібно зробити рядок таблиці перетягуваним, додавши:
1. Атрибут `draggable`
2. Обробник події `dragStart` (Event Listener)
dragStart(e, index)}
```
Тепер, що ми маємо робити в функції dragStart
? Нам потрібно зберігати посилання на той рядок, який перетягується. Є кілька способів зробити це.
Якщо ми працюємо в світі, де не використовується React, ми можемо налаштувати це за допомогою методу dataTransfer.setData
, як показано нижче, і пізніше отримати дані за допомогою getData
.
function dragStart(e: React.DragEvent, index: number) {
e.dataTransfer.setData("application/json", JSON.stringify(pokemons[index]))
}
Але!
Давайте створимо змінну стану для цього, тому що з нею набагато зручніше працювати, а ще є кілька місць, де ми будемо використовувати її пізніше!
const [draggingIndex, setDraggingIndex] = React.useState(null)
Тепер ми можемо встановити її в dragStart
.
function dragStart(_e: React.DragEvent, index: number) {
setDraggingIndex(index)
}
Так! Тепер ми можемо перетягувати наші рядки!
Перш ніж перейти до скидання, я б хотів поділитися з вами кількома варіантами налаштування поведінки перетягування.
Налаштування перетягування
По-перше, за замовчуванням, при dragStart
, автоматично генерується напівпрозоре зображення з цільового елемента (того елемента, на якому спрацьовує подія dragStart
), яке з’являється поруч з курсором під час операції перетягування.
Якщо ми хочемо використовувати власне зображення замість цього, ми можемо налаштувати його за допомогою методу setDragImage
, як показано нижче.
const img = new Image();
img.src = "https://i.pinimg.com/736x/d1/23/5d/d1235dba2efc668cf4bd3c6bf82c00cd.jpg";
e.dataTransfer.setDragImage(img, 10, 10);
Джерело може бути або віддаленим URL, або локальним файлом.
До речі, це не працює в Safari! Принаймні у мене! У Chrome все працює добре…
Документація MDN стверджує, що воно має працювати. Але ні…
Ми також можемо контролювати, який курсор браузер відображає під час операції перетягування, за допомогою властивості dropEffect
.
Є три ефекти на вибір.
copy
означає, що перетягнуті дані будуть скопійовані з поточного місця в місце скидання.move
означає, що перетягнуті дані будуть переміщені з поточного місця в місце скидання.link
означає, що буде створено якусь форму зв'язку чи відносин між вихідним та цільовим місцями.
Ми можемо налаштувати це так:
e.dataTransfer.dropEffect = "move";
Місце скидання
Щоб зробити рядок місцем для скидання, ми будемо визначати подію dragEnter
(Event Listener).
dragStart(e, index)}
onDragEnter={(e) => dragEnter(e, index)}
>
І в межах цієї події ми будемо оновлювати як pokemons
, так і draggingIndex
.
function dragEnter(e: React.DragEvent, index: number) {
if (draggingIndex === null || index === draggingIndex) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setPokemons((prevState) => {
let newPokemons = [...prevState];
const deleteElement = newPokemons.splice(draggingIndex, 1)[0];
newPokemons.splice(index, 0, deleteElement);
return newPokemons;
});
setDraggingIndex(index);
}
Зверніть увагу, що я викликаю preventDefault
тут, щоб запобігти додатковій обробці події, такій як події торкання або події вказівного пристрою.
Знаю, як ви бачите, коли ми завершуємо або скасовуємо операцію перетягування і скидання, ми отримуємо ефект повернення на рядку, який перетягується, що насправді не дуже бажано.
Ми вирішимо це за кілька секунд, але перед тим давайте швидко додамо подію dragEnd
.
Закінчення перетягування
dragEnd
спрацьовує, коли операція перетягування завершується, або після відпускання кнопки миші, або при натисканні клавіші Escape.
Це чудове місце для скидання станів, збереження даних, наприклад, через запит до сервера, і так далі.
dragEnd(e)}
>
Ми лише скинемо dragIndex
назад на null тут.
function dragEnd(e: React.DragEvent) {
setDraggingIndex(null)
}
Усунення ефекту повернення
Насправді є два випадки, які потрібно врахувати.
По-перше, давайте розглянемо випадок, коли операція перетягування завершилася успішно. Щоб усунути ефект, все, що нам потрібно зробити — це викликати preventDefault
на події dragOver
.
dragStart(e, index)}
onDragEnter={(e) => dragEnter(e, index)}
onDragOver={(e) => e.preventDefault()}
onDragEnd={(e) => dragEnd(e)}
>
Однак, це тільки запобігає ефекту на операціях перетягування, які були успішно завершені, але не якщо перетягування було скасоване.
Це тому, що ми МУСИМО повернути дані до їхнього початкового стану, якщо перетягування було скасоване!
Отже!
Замість того, щоб намагатися усунути поведінку в цьому випадку, давайте покращимо нашу реалізацію, щоб скинути дані назад до стану до початку операції.
Є два способи підійти до цього!
- Замість того, щоб змінювати дані в
dragEnter
, використовуйтеdragOver
+drop
, і змінюйте дані тільки приdrop
. - Зберігайте посилання на попередні дані і відновлюйте їх, якщо операція перетягування була скасована.
Я виберу другий підхід, оскільки мені подобається поведінка сортування, коли ми перетягуємо на область скидання.
Щоб визначити, чи завершена або скасована операція перетягування, ми можемо використовувати onEnd
, перевіряючи властивість dropEffect
. Якщо вона має значення none
під час dragend
, то перетягування було скасоване. В іншому випадку, ефект вказує на те, яка операція була виконана.
Ми додамо ще одну змінну стану тут, щоб відстежувати стан до початку перетягування і оновлювати її відповідно до кожної обробки події.
const [previousPokemons, setPreviousPokemons] = React.useState(initailPokemons)
function dragStart(e: React.DragEvent, index: number) {
e.dataTransfer.dropEffect = "move";
setDraggingIndex(index)
setPreviousPokemons([...pokemons])
}
function dragEnter(e: React.DragEvent, index: number) {
if (draggingIndex === null || index === draggingIndex) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setPokemons((prevState) => {
let newPokemons = prevState;
const deleteElement = newPokemons.splice(draggingIndex, 1)[0];
newPokemons.splice(index, 0, deleteElement);
return newPokemons;
});
setDraggingIndex(index);
}
function dragEnd(e: React.DragEvent) {
setDraggingIndex(null)
if (e.dataTransfer.dropEffect === "none") {
setPokemons([...previousPokemons])
} else {
setPreviousPokemons([...pokemons])
}
}
Як бачите, якщо ми вирішуємо скасувати подію, рядок повертається на своє місце.
Маленька примітка
Завжди створюйте новий масив при присвоєнні стану замість того, щоб використовувати змінну стану з іншого стану, тобто: setPokemons([…previousPokemons])
, setPreviousPokemons([…pokemons])
, щоб pokemons
та previousPokemons
не вказували на один і той самий об'єкт.
Маленька дискусія
Можливо, ви дивуєтесь, чому я не сортував за допомогою dragOver
замість dragEnter
.
Поводження може здаватися схожим.
Однак, якщо ми виведемо обидві події в консоль, ми побачимо, що
- подія
dragEnter
спрацьовує, як тільки ми перетягуємо елемент в цільову область, і лише один раз
тоді як
- подія
dragOver
спрацьовує протягом всього часу, поки ми перетягуємо елемент в межах області скидання, поки не вийдемо з цієї області або не скинемо елемент, кожні десяті частки секунди
Не потрібно в даному випадку!
Трошки додаткового стилю
Щоб завершити нашу сьогоднішню роботу, давайте додамо трохи стилю до рядка, який перетягується, щоб він виділявся!
Це просто: все, що нам потрібно зробити, це перевірити, чи є draggingIndex
таким самим, як Index
рядка, і саме тому ми використовуємо змінну стану замість того, щоб просто встановити дані через dataTransfer
!
Наприклад, додамо інший фон!
Підсумки
Ось код на сьогодні!
'use client'
import React from "react"
export type Pokemon = {
id: number
name: string
}
const initailPokemons: Pokemon[] = [
{ id: 1, name: "bulbasaur" },
{ id: 2, name: "ivysaur" },
{ id: 3, name: "venusaur" },
{ id: 4, name: "charmander" },
{ id: 5, name: "charmeleon" },
{ id: 6, name: "charizard" },
{ id: 7, name: "squirtle" },
{ id: 8, name: "blastoise" },
{ id: 9, name: "caterpie" },
{ id: 10, name: "metapod" },
] as const
export default function Demo() {
const [pokemons, setPokemons] = React.useState(initailPokemons)
const [draggingIndex, setDraggingIndex] = React.useState(null)
const [previousPokemons, setPreviousPokemons] = React.useState(initailPokemons)
function dragStart(e: React.DragEvent, index: number) {
e.dataTransfer.dropEffect = "move";
setDraggingIndex(index)
setPreviousPokemons([...pokemons])
}
function dragEnter(e: React.DragEvent, index: number) {
if (draggingIndex === null || index === draggingIndex) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setPokemons((prevState) => {
let newPokemons = prevState;
const deleteElement = newPokemons.splice(draggingIndex, 1)[0];
newPokemons.splice(index, 0, deleteElement);
return newPokemons;
});
setDraggingIndex(index);
}
function dragEnd(e: React.DragEvent) {
setDraggingIndex(null)
if (e.dataTransfer.dropEffect === "none") {
setPokemons([...previousPokemons])
} else {
setPreviousPokemons([...pokemons])
}
}
return (
Pokemons
{pokemons.map((pokemon, index) => ( dragStart(e, index)} onDragEnter={(e) => dragEnter(e, index)} onDragOver={(e) => e.preventDefault()} onDragEnd={(e) => dragEnd(e)} className={index === draggingIndex ? "bg-default" : ""} > ))}
No. Name {pokemon.id} {pokemon.name}
); } ``` Дякую за прочитання! Це все на сьогодні! Знаю! Перетягування завжди найкраще! (Будь ласка, не кричіть на мене, якщо ви не погоджуєтесь!)
Перекладено з: [React/Typescript: Table Row Re-Ordering with Drag and Drop](https://levelup.gitconnected.com/react-table-row-re-ordering-with-drag-and-drop-fb7105baafb7)