Вот код, который я сделал для реализации выбора перетаскивания.
Код генерации элемента
const items = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103,
104,
].map((i) => ({ item: i, selected: i === 1 }));
Это фактический код, отвечающий за выбор
const [data, setData] = useState(items);
const [isSelecting, setIsSelecting] = useState(false);
const [start, setStart] = useState<Coords>({ x: 0, y: 0, screenX: 0, screenY: 0 });
const [end, setEnd] = useState<Coords>({ x: 0, y: 0, screenX: 0, screenY: 0 });
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleMouseDown(e: any) {
if (e.target.closest(".selectable")) return;
setIsSelecting(true);
setStart({ x: e.clientX, y: e.clientY, screenX: e.screenX, screenY: e.screenY });
setEnd({ x: e.clientX, y: e.clientY, screenX: e.screenX, screenY: e.screenY });
setData((data) => [...data.map((item) => ({ ...item, selected: false }))]);
}
ref.current?.addEventListener("mousedown", handleMouseDown);
return () => {
ref.current?.removeEventListener("mousedown", handleMouseDown);
};
}, [ref]);
function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
if (!isSelecting) return;
console.info("START");
console.info({ clientX: start.x, clientY: start.y, screenX: start.screenX, screenY: start.screenY });
console.info("END");
console.info({ clientX: e.clientX, clientY: e.clientY, screenX: e.screenX, screenY: e.screenY });
setEnd({ x: e.clientX, y: e.clientY, screenX: e.screenX, screenY: e.screenY });
const selected = [...data];
const elements = document.getElementsByClassName("selectable");
for (let i = 0; i < elements.length; i++) {
const rect = elements[i].getBoundingClientRect();
const elementRect = {
left: rect.left + window.pageXOffset,
top: rect.top + window.pageYOffset,
right: rect.right + window.pageXOffset,
bottom: rect.bottom + window.pageYOffset,
};
if (
((elementRect.left >= Math.min(start.x, end.x) && elementRect.left <= Math.max(start.x, end.x)) ||
(elementRect.right >= Math.min(start.x, end.x) && elementRect.right <= Math.max(start.x, end.x))) &&
((elementRect.top >= Math.min(start.y, end.y) && elementRect.top <= Math.max(start.y, end.y)) ||
(elementRect.bottom >= Math.min(start.y, end.y) && elementRect.bottom <= Math.max(start.y, end.y)))
) {
selected[i].selected = true;
} else {
selected[i].selected = false;
}
}
setData(selected);
}
function handleMouseUp() {
setIsSelecting(false);
reset();
}
const reset = () => {
setStart({ x: 0, y: 0, screenX: 0, screenY: 0 });
setEnd({ x: 0, y: 0, screenX: 0, screenY: 0 });
};
Создать наложение
const overlayStyle: any = {
position: "absolute",
backgroundColor: colors.slate[800],
opacity: 0.5,
border: "1px dotted",
borderColor: colors.slate[300],
left: `${Math.min(start.x, end.x) - (ref.current?.offsetLeft || 0)}px`,
top: `${Math.min(start.y, end.y) - (ref.current?.offsetTop || 0) + (ref.current?.scrollTop || 0)}px`,
width: `${Math.abs(end.x - start.x)}px`,
height: `${Math.abs(end.y - start.y) + (ref.current?.scrollTop || 0)}px`,
display: isSelecting ? "block" : "none",
pointerEvents: "none",
};
Шаблон JSX
return (
<>
<div onMouseUp = {handleMouseUp} onMouseMove = {handleMouseMove} className = "relative p-4 overflow-auto bg-slate-600 h-96" ref = {ref}>
<ul className = "flex flex-wrap gap-2">
{data.map((item) => (
<li
onClick = {() => {
console.info("a");
}}
className = {cx(
"flex items-center justify-center w-24 text-white hover:border-4 hover:border-slate-500 rounded-lg select-none aspect-square bg-slate-700 selectable cursor-pointer",
{ "border-sky-500 border-4 hover:border-sky-500": item.selected }
)}
key = {item.item}
>
Item {item.item}
</li>
))}
</ul>
<div style = {overlayStyle}></div>
</div>
<button
onClick = {() => {
console.info(ref);
}}
>
1a
</button>
</>
);
Все работает корректно,
Но дело в том, что когда элементы значимы и показывают прокрутку, он не будет работать как выбор, так и наложение выбора.
Спасибо за ваше время!



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


фактически вы сравниваете положение документа с положением на экране. Ваши clientX и screenX будут в основном одинаковыми для мыши, если ваш элемент занимает весь экран.
Когда элемент прокручивается за пределы верхней части экрана, он будет иметь отрицательную клиентскую координату. Вы добавляете позицию прокрутки, которая указывает позицию элемента в документе.
Однако координаты вашей мыши не смещаются таким же образом.
Вы также, кажется, не используете правильное значение прокрутки. У вас есть элемент прокрутки, но вы добавляете позицию прокрутки окна, которая не прокручивается. Используйте scrollX и scrollY элемента, а не окна.
Вы должны иметь возможность сохранить координаты мыши как смещения документа, добавив к ним также смещения прокрутки, тогда вы будете сравнивать все как координаты документа.
Используйте смещение прокрутки во время сохранения координат, поэтому
setStart({ x: e.clientX + ref.current.scrollLeft, y: e.clientY + ref.current.scrollTop });
Вы также, вероятно, должны использовать состояние для конечной позиции. На самом деле он не будет обновлять координаты до следующего рендеринга, поэтому ваш выбор будет на 1 рендер и движение мыши позади. Вы уже заметили и задались вопросом, почему?
Делать:
const endX = e.clientX + ref.current.scrollLeft;
const endY = e.clientY + ref.current.scrollTop;
setEnd({x: endX, y: endY});
и используйте эти значения в расчетах ниже. Они будут более актуальными (вам все еще нужно сделать setEnd для вашего оверлея, но ваш расчет оверлея тоже должен измениться. Если он имеет абсолютное позиционирование, он должен прокручиваться вместе с окном, поэтому просто используйте позицию документа и не беспокойтесь. об вырезании его на экран)
const elementRect = {
left: rect.left + ref.current.scrollLeft,
top: rect.top + ref.current.scrollTop,
right: rect.right + ref.current.scrollLeft,
bottom: rect.bottom + ref.current.scrollTop,
};
selected[i].selected = ((elementRect.left >= Math.min(start.x, endX) && elementRect.left <= Math.max(start.x, endX)) ||
(elementRect.right >= Math.min(start.x, endX) && elementRect.right <= Math.max(start.x, endX))) &&
((elementRect.top >= Math.min(start.y, endY) && elementRect.top <= Math.max(start.y, endY)) ||
(elementRect.bottom >= Math.min(start.y, endY) && elementRect.bottom <= Math.max(start.y, endY)))
);
// for your overlay, if you make it a child of your main
// element, it will work better (set zIndex). Otherwise,
// you need to do a more complex rectangle intersection than this.
left: `${Math.min(start.x, end.x)}px`,
top: `${Math.min(start.y, end.y)}px`,
width: `${Math.abs(end.x - start.x)}px`,
height: `${Math.abs(end.y - start.y)}px`,