我正在尝试从头开始在React中实现拖拽选择,但卡在滚动部分。

huangapple go评论75阅读模式
英文:

I am trying to implement drag select in react from scratch but stuck in the scroll

问题

以下是我为您翻译的代码部分:

这是我制作的用于实现拖选的代码。

生成项目代码
```tsx
const items = [
    // ...(省略了很多项目)
].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;
    // ...(省略了部分代码)
}

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.log("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}
                    </li>
                ))}
            </ul>
            <div style={overlayStyle}></div>
        </div>

        <button
            onClick={() => {
                console.log(ref);
            }}
        >
            1a
        </button>
    </>
);

所有部分都正常工作,但当元素很多且显示滚动条时,选择以及选择叠加层将无法正常工作。

英文:

Here is the code that I made to implement drag selection.

Item generate code


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) =&gt; ({ item: i, selected: i === 1 }));

This the actual code responsible for the select

    const [data, setData] = useState(items);
    const [isSelecting, setIsSelecting] = useState(false);
    const [start, setStart] = useState&lt;Coords&gt;({ x: 0, y: 0, screenX: 0, screenY: 0 });
    const [end, setEnd] = useState&lt;Coords&gt;({ x: 0, y: 0, screenX: 0, screenY: 0 });
    const ref = useRef&lt;HTMLDivElement&gt;(null);
    useEffect(() =&gt; {
        function handleMouseDown(e: any) {
            if (e.target.closest(&quot;.selectable&quot;)) 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) =&gt; [...data.map((item) =&gt; ({ ...item, selected: false }))]);
        }
        ref.current?.addEventListener(&quot;mousedown&quot;, handleMouseDown);
        return () =&gt; {
            ref.current?.removeEventListener(&quot;mousedown&quot;, handleMouseDown);
        };
    }, [ref]);

    function handleMouseMove(e: React.MouseEvent&lt;HTMLDivElement&gt;) {
        if (!isSelecting) return;
        console.log(&quot;START&quot;);
        console.log({ clientX: start.x, clientY: start.y, screenX: start.screenX, screenY: start.screenY });
        console.log(&quot;END&quot;);
        console.log({ 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(&quot;selectable&quot;);
        for (let i = 0; i &lt; 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 &gt;= Math.min(start.x, end.x) &amp;&amp; elementRect.left &lt;= Math.max(start.x, end.x)) ||
                    (elementRect.right &gt;= Math.min(start.x, end.x) &amp;&amp; elementRect.right &lt;= Math.max(start.x, end.x))) &amp;&amp;
                ((elementRect.top &gt;= Math.min(start.y, end.y) &amp;&amp; elementRect.top &lt;= Math.max(start.y, end.y)) ||
                    (elementRect.bottom &gt;= Math.min(start.y, end.y) &amp;&amp; elementRect.bottom &lt;= Math.max(start.y, end.y)))
            ) {
                selected[i].selected = true;
            } else {
                selected[i].selected = false;
            }
        }
        setData(selected);
    }

    function handleMouseUp() {
        setIsSelecting(false);

        reset();
    }
    const reset = () =&gt; {
        setStart({ x: 0, y: 0, screenX: 0, screenY: 0 });
        setEnd({ x: 0, y: 0, screenX: 0, screenY: 0 });
    };

Generate the overlay

const overlayStyle: any = {
        position: &quot;absolute&quot;,
        backgroundColor: colors.slate[800],
        opacity: 0.5,
        border: &quot;1px dotted&quot;,
        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 ? &quot;block&quot; : &quot;none&quot;,
        pointerEvents: &quot;none&quot;,
    };

JSX Templete


    return (
        &lt;&gt;
            &lt;div onMouseUp={handleMouseUp} onMouseMove={handleMouseMove} className=&quot;relative p-4 overflow-auto bg-slate-600 h-96&quot; ref={ref}&gt;
                &lt;ul className=&quot;flex flex-wrap gap-2&quot;&gt;
                    {data.map((item) =&gt; (
                        &lt;li
                            onClick={() =&gt; {
                                console.log(&quot;a&quot;);
                            }}
                            className={cx(
                                &quot;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&quot;,
                                { &quot;border-sky-500 border-4 hover:border-sky-500&quot;: item.selected }
                            )}
                            key={item.item}
                        &gt;
                            Item {item.item}
                        &lt;/li&gt;
                    ))}
                &lt;/ul&gt;
                &lt;div style={overlayStyle}&gt;&lt;/div&gt;
            &lt;/div&gt;

            &lt;button
                onClick={() =&gt; {
                    console.log(ref);
                }}
            &gt;
                1a
            &lt;/button&gt;
        &lt;/&gt;
    );

All are working correctly,

But the thing is when the elements are significant and show scroll it will not working select as well as the selection overlay.

我正在尝试从头开始在React中实现拖拽选择,但卡在滚动部分。

Thank you for your time!

答案1

得分: 1

你实际上正在比较文档位置和屏幕位置。如果您的元素占据整个屏幕,那么您的 clientX 和 screenX 基本上是相同的。

当一个元素滚动到屏幕顶部时,它将具有负的 client 坐标。您正在添加滚动位置,这会给出元素在文档中的位置。

然而,您的鼠标坐标没有以相同的方式偏移。

您似乎也没有使用正确的滚动值。您有一个滚动元素,但却添加了窗口滚动位置,这不会滚动。请使用元素的 scrollXscrollY,而不是窗口的。

您应该能够通过添加滚动偏移来将鼠标坐标保存为文档偏移,然后您将以文档坐标比较所有内容。

在保存坐标时使用滚动偏移,所以

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)))
);

// 对于您的覆盖层,如果将其设置为主元素的子元素,它将工作得更好(设置 zIndex)。否则,您需要执行比这更复杂的矩形交叉检查。
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`,
英文:

you are, in effect, comparing document position to screen position. Your clientX and screenX will basically be the same for mouse, if your element occupies the full screen.

When an element scrolls off the top of screen, it will have a negative client coordinate. You are adding the scroll position, which gives the position of the element in the document.

However, your mouse coordinates are not being offset the same way.

You also don't seem to be using the right scroll value. You have a scrolling element, but are adding the window scroll position, which doesn't scroll. Use the scrollX and scrollY of the element, not the window.

You should be able to save the mouse coordinates as document offsets by adding the scrolling offsets to those as well, then you will be comparing everything as document coordinates.

Use the scroll offset at the time you save the coordinates, so

setStart({ x: e.clientX + ref.current.scrollLeft, y: e.clientY + ref.current.scrollTop });

You also probably should use state for the end position. It won't actually update the coordinate until the following render, so your selection will be 1 render and mouse move behind. Have you already noticed and wondered why?

Do:

const endX = e.clientX + ref.current.scrollLeft;
const endY = e.clientY + ref.current.scrollTop;
setEnd({x: endX, y: endY});

and use those values in the calculations below. They will be more up to date (you still need to do setEnd for your overlay, but your overlay calculation should change, too. if it is absolute positioned it should scroll with the window, so just use the document position and don't worry about clipping it to the screen)

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 &gt;= Math.min(start.x, endX) &amp;&amp; elementRect.left &lt;= Math.max(start.x, endX)) ||
(elementRect.right &gt;= Math.min(start.x, endX) &amp;&amp; elementRect.right &lt;= Math.max(start.x, endX))) &amp;&amp;
((elementRect.top &gt;= Math.min(start.y, endY) &amp;&amp; elementRect.top &lt;= Math.max(start.y, endY)) ||
(elementRect.bottom &gt;= Math.min(start.y, endY) &amp;&amp; elementRect.bottom &lt;= 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`,

huangapple
  • 本文由 发表于 2023年5月11日 18:22:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/76226581.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定