Фрагмент ниже содержит массив из 10 элементов. Я могу перетаскивать элементы списка и даже могу добиться некоторых базовых анимаций при захвате элемента списка:
const App = () => {
const [myArray, setMyArray] = React.useState([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const [draggedIndex, setDraggedIndex] = React.useState(-1);
const onDragStart = (e, index) => {
setDraggedIndex(index);
const emptyDiv = document.createElement('div');
emptyDiv.style.width = '0px';
emptyDiv.style.height = '0px';
e.dataTransfer.setDragImage(emptyDiv, 0, 0);
e.currentTarget.className = 'draggable';
};
const onMouseDown = (e) => {
e.currentTarget.className = 'draggable';
};
const onMouseUp = (e) => {
e.currentTarget.className = 'listItem';
};
const onDragOver = (e, index) => {
e.preventDefault();
if (draggedIndex === -1 || draggedIndex === index) {
return;
}
let items = myArray.filter((item, i) => i !== draggedIndex);
items.splice(index, 0, myArray[draggedIndex]);
setMyArray(items);
setDraggedIndex(index);
};
const onDragEnd = (e) => {
setDraggedIndex(-1);
e.target.className = 'listItem';
};
return (
<div className = "App">
{myArray.map((x, i) => (
<div
className = "listItem"
draggable
key = {x}
onDragStart = {(e) => onDragStart(e, i)}
onDragOver = {(e) => onDragOver(e, i)}
onDragEnd = {onDragEnd}
onMouseDown = {onMouseDown}
onMouseUp = {onMouseUp}
>
<h3>hello - {x}</h3>
</div>
))}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));.App {
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
}
.listItem {
border: 2px solid black;
margin: 5px;
width: 400px;
cursor: grab;
transform: scale(100%);
transition: transform 0.3s ease-in-out;
}
.draggable {
border: 2px solid green;
margin: 5px;
width: 400px;
cursor: grab;
transform: scale(108%);
transition: transform 0.3s ease-in-out;
}
.listItem:-moz-drag-over {
cursor: pointer;
}<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id = "root"></div>Могу ли я получить помощь с анимацией CSS, чтобы сделать движения элементов списка более плавными во время перетаскивания, чтобы они выглядели менее прерывистыми? Цель состоит в том, чтобы добиться следующего эффекта: когда я перетаскиваю элемент, он плавно перемещается вверх/вниз, а перетаскиваемый элемент плавно перемещается в противоположном направлении.
По сути, я хочу анимировать переход перетаскиваемого элемента в зависимости от того, в каком направлении (вверх или вниз) перетаскивается элемент. Теоретически, при перетаскивании элемента вверх он может применить класс, например .dragged-up, и этот класс будет иметь анимацию/переход, который создаст иллюзию перемещения этого элемента из более низкого положения в более высокое.
Тот же принцип можно применить к элементам выше и ниже перетаскиваемого элемента. Например, если перетаскиваемый элемент перемещается сверху вниз, может быть применен другой класс, например .listItem-down, и этот класс может содержать противоположную анимацию. Кроме того, я подозреваю, что у него должен быть более низкий z-индекс, чтобы он отображался под перетаскиваемым элементом.
Не уверен, что это самый эффективный подход и возможно ли это сделать вообще. До сих пор, пытаясь реализовать что-то подобное, у меня возникали проблемы с перекрытием элементов, и в результате функция события выполнялась не в том div, вызывая некоторые нежелательные эффекты.
Некоторая помощь и рабочий фрагмент будут высоко оценены!
Ах, это дополнительная проблема, которую я упустил из виду, я предположил, что из-за простоты примера это не должно быть проблемой. Я тоже только что заметил, что работает только при перетаскивании с помощью мыши, но не тачскрина на телефоне. Интересно, будет ли логика кода такой же для мобильных устройств, просто другие события, такие как onTouchMove вместо перетаскивания?
Ах, теперь я снова думаю об этом и понял, что, возможно, Safari запускает разные события при нажатии? Например, OnTouch вместо onClick
Нужно ли в решении использовать setMyArray для перестановки, а также нужно ли вам иметь возможность устанавливать элементы, просто расширяя массив, или их можно устанавливать статически?
Что касается setMyArray - изначально я думал, что да, но, поскольку вы спросили, я не уверен, что это лучший подход. Это может помочь, если я объясню общую картину. Я создаю компонент, который будет иметь раскрывающийся список и список. Список будет заполнен значениями, выбранными из раскрывающегося списка. Список элементов в раскрывающемся списке будет статическим. Массив списка будет расширяемым, а также переупорядочиваемым путем перетаскивания. Компонент будет частью формы. При создании новой формы мы бы начали с пустого списка. Мы также должны иметь возможность загружать форму из формы API и восстанавливать список из данных.
Не уверен, что это имеет смысл, но в конечном итоге компонент должен иметь возможность получать массив элементов, пользователь должен иметь возможность переупорядочивать элементы и передавать переупорядоченные данные обратно. Дайте мне знать, если вам нужна дополнительная информация



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Этот ответ вдохновлен этим решением и пытается сделать значительно упрощенный перенос своих основных идей на функциональные компоненты React, которые работают для перетаскиваемых элементов в случае использования.
В опубликованном примере порядок элементов в массиве обновляется при каждом перетаскивании. Чтобы создать переход при изменении порядка, можно обнаружить разницу до и после изменения для каждого элемента и использовать ее в качестве начальной и конечной точек анимации.
В следующем подходе каждому элементу назначается ключ ref, чтобы отслеживать обновления и проверять изменения в их отображаемой позиции с помощью getBoundingClientRect в useLayoutEffect, чтобы можно было предпринять дальнейшие действия до того, как браузер перерисует экран.
Чтобы вычислить различия, положение элементов в последнем рендере prevPos сохраняется отдельно как другое ref, чтобы оно сохранялось между рендерами. В этом упрощенном примере проверяется и вычисляется только top позиция для разницы, чтобы создать смещение для translateY.
Затем, чтобы организовать переход, requestAnimationFrame вызывается два раза, при этом первый кадр визуализирует элементы в смещенных позициях (начальная точка, со смещением в translateY), а второй — в новых естественных позициях (конечная точка, с 0 в translateY). ).
Хотя на данный момент useLayoutEffect уже обрабатывает анимацию, как и ожидалось, тот факт, что onDragOver очень часто запускает и обновляет состояние, может легко вызвать ошибки в отображении движения.
Я попытался реализовать некоторую базовую отмену дребезга для обновления массива состояний и ввел еще один useEffect для обработки обновлений с отменой дребезга, но кажется, что эффекты могут иногда быть нестабильными.
Хотя многое еще можно было бы улучшить, вот экспериментальный пример:
const App = () => {
const [myArray, setMyArray] = React.useState([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
]);
const [draggedKey, setDraggedKey] = React.useState(null);
const [pendingNewKey, setPendingNewKey] = React.useState(null);
const elRef = React.useRef({});
const keyInAnimation = React.useRef(null);
const prevPos = React.useRef({});
// 👇 Attempt to debounce update of array
React.useEffect(() => {
if (
pendingNewKey === null ||
draggedKey === null ||
draggedKey === pendingNewKey ||
keyInAnimation.current === draggedKey
)
return;
const updateArray = () => {
setMyArray((prev) => {
const prevIndex = prev.findIndex((x) => x === draggedKey);
const newIndex = prev.findIndex((x) => x === pendingNewKey);
const newArray = [...prev];
newArray[prevIndex] = pendingNewKey;
newArray[newIndex] = draggedKey;
return newArray;
});
};
const debouncedUpdate = setTimeout(updateArray, 100);
return () => clearTimeout(debouncedUpdate);
}, [pendingNewKey, draggedKey]);
React.useLayoutEffect(() => {
Object.entries(elRef.current).forEach(([key, el]) => {
if (!el) return;
// 👇 Get difference in position to calculate an offset for transition
const { top } = el.getBoundingClientRect();
if (!prevPos.current[key] && prevPos.current[key] !== 0)
prevPos.current[key] = top;
const diffTop = Math.floor(prevPos.current[key] - top);
if (diffTop === 0 || Math.abs(diffTop) < 30) return;
prevPos.current[key] = top;
el.style.transform = `translateY(${diffTop}px)`;
el.style.transition = 'scale 0.3s ease-in-out, transform 0s';
// 👇 First frame renders offset positions, second the transition ends
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (!el) return;
el.style.transform = `translateY(0px)`;
el.style.transition =
'scale 0.3s ease-in-out, transform 100ms ease-out';
});
});
});
}, [myArray.toString()]);
const onDragStart = (e, key) => {
keyInAnimation.current = key;
setDraggedKey(key);
const emptyDiv = document.createElement('div');
emptyDiv.style.width = '0px';
emptyDiv.style.height = '0px';
e.dataTransfer.setDragImage(emptyDiv, 0, 0);
e.currentTarget.className = 'draggable';
};
const onMouseDown = (e) => {
e.currentTarget.className = 'draggable';
};
const onMouseUp = (e) => {
e.currentTarget.className = 'listItem';
};
const onDragOver = (e, key) => {
e.preventDefault();
if (draggedKey === null) return;
if (draggedKey === key) {
keyInAnimation.current = key;
setPendingNewKey(null);
return;
}
if (keyInAnimation.current === key) {
return;
}
keyInAnimation.current = key;
setPendingNewKey(key);
// 👇 Attempt to reduce motion error but could be unnecessary
Object.values(elRef.current).forEach((el) => {
if (!el) return;
el.style.transform = `translateY(0px)`;
el.style.transition = 'scale 0.3s ease-in-out, transform 0s';
});
};
const onDragEnd = (e) => {
setDraggedKey(null);
setPendingNewKey(null);
keyInAnimation.current = null;
e.target.className = 'listItem';
};
return (
<div className = "App">
{myArray.map((x) => (
<div
className = "listItem"
draggable
key = {x}
onDragStart = {(e) => onDragStart(e, x)}
onDragOver = {(e) => onDragOver(e, x)}
onDragEnd = {onDragEnd}
onMouseDown = {onMouseDown}
onMouseUp = {onMouseUp}
ref = {(el) => (elRef.current[x] = el)}
>
<h3>hello - {x}</h3>
</div>
))}
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#root"));.App {
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
isolation: isolate;
gap: 15px;
}
.listItem {
border: 2px solid black;
margin: 5px;
width: 400px;
cursor: grab;
z-index: 1;
transition: scale 0.3s ease-in-out;
background-color: white;
}
.draggable {
border: 2px solid hotpink;
margin: 5px;
width: 400px;
cursor: grab;
scale: 108%;
z-index: 10;
transition: scale 0.3s ease-in-out;
background-color: white;
}
.listItem:-moz-drag-over {
cursor: pointer;
}<div id = "root"></div>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>Раствор твердый. Я не думаю, что вы могли бы помочь мне с красным крестом мыши, когда я перетаскиваю? Не могу найти, почему это так
@SebastianMeckovski Вероятно, это связано с тем, что контейнер не является целью onDragOver, и это следует предотвратить, добавив в контейнер событие, такое как onDragOver = {(e) => e.preventDefault()} (здесь контейнер «Приложение»). Однако в этом примере, поскольку перетаскивание вступает в силу только при перетаскивании элемента, я думаю, было бы неплохо рассмотреть возможность сохранения значка креста, поскольку он указывает эффективную область для перетаскивания.
Вы были правы, добавив onDragOver = {(e) => e.preventDefault()}. предотвращает превращение мыши в красный крест, я на самом деле предпочитаю это. Это на шаг ближе. По-прежнему превращается в значок красного креста при наведении курсора на границу элемента списка.
@SebastianMeckovski Спасибо за отзыв, мне кажется, в Firefox все в порядке, но когда я пытаюсь использовать Chrome, он действительно показывает значок креста и снова мигает. Вероятно, из-за различий в способах обработки браузерами (относительно новой) функции перетаскивания. До сих пор я думаю, что если мы добавим h3 { pointer-events: none } в CSS, это предотвратит это для границы h3 (текстового поля «Привет»), но поскольку мы не можем добавить то же самое ко всему элементу, не уверен, что это будет можно указать некоторым браузерам отключить это поведение.
Просто отметим: я не изучал, почему, и не вижу никаких сообщений об ошибках, но что-то в вашем фрагменте не работает в текущем Safari (16.1); событие щелчка срабатывает, чтобы дать небольшую анимацию «импульс», но операции перетаскивания - нет. У меня отлично работает как в хроме, так и в Firefox.