React DragBox

Date: 2024-03-04
import React, { useRef, MouseEvent, ReactNode, useState, RefObject, useContext, useEffect } from "react";
import { keyStateProvider } from "../../../helpers/KeyStateProvider";
import { Observable, Subject, Subscription } from "rxjs";
import { clearTextSelection } from "../../../helpers/tshelper";

export interface DragArgs {
    parentRect: DOMRect;
    rect: DOMRect
}

export const DragBoxContext = React.createContext<Observable<DragArgs> | undefined>(undefined);

function checkIsIntersecting(parent: DOMRect, child: DOMRect) {
    return parent.left <= child.right && parent.right >= child.left && parent.top <= child.bottom && parent.bottom >= child.top;
}

export function useDragSelect(elementRef: RefObject<HTMLElement>, disabled?: boolean) {
    const dragObservable = useContext(DragBoxContext);
    const [isSelected, setIsSelected] = useState<boolean>(false);
    useEffect(() => {
        if (disabled) return;
        if (!dragObservable) return;
        const subscription: Subscription = dragObservable.subscribe((args: DragArgs) => {
            const e = elementRef.current;
            if (!e) return;
            if (keyStateProvider.keyStates.shift && isSelected) return;
            const bRect = e.getBoundingClientRect();
            const pRect = args.parentRect;
            const rect2 = new DOMRect(bRect.x - pRect.x, bRect.y - pRect.y, bRect.width, bRect.height);
            const isIntersecting = checkIsIntersecting(args.rect, rect2);
            setIsSelected(isIntersecting);
        });
        return () => subscription.unsubscribe();
    }, [dragObservable, elementRef, isSelected, disabled]);
    return isSelected && !disabled;
}

export function DragBox(props: { children: ReactNode }) {
    const { children } = props;
    const [subject] = useState(new Subject<DragArgs>())
    const containerRef = useRef<HTMLDivElement>(null);
    const draggingRef = useRef(false);
    const startX = useRef(0);
    const startY = useRef(0);

    function getDragRect(e: MouseEvent) {
        if (draggingRef.current && containerRef.current) {
            const rect = containerRef.current.getBoundingClientRect();
            const width = Math.abs(e.clientX - startX.current);
            const height = Math.abs(e.clientY - startY.current);
            const left = Math.min(startX.current, e.clientX) - rect.left;
            const top = Math.min(startY.current, e.clientY) - rect.top;
            const dragRect = new DOMRect(left, top, width, height);
            return dragRect;
        }
        return undefined;
    }

    const onMouseDown = (e: MouseEvent<HTMLDivElement>) => {
        if (keyStateProvider.keyStates.control) return;
        e.preventDefault();
        clearTextSelection();
        draggingRef.current = true;
        startX.current = e.clientX;
        startY.current = e.clientY;
        window.addEventListener("mousemove", onMouseMove as any);
        window.addEventListener("mouseup", onMouseUp as any);
    };

    const onMouseMove = (e: MouseEvent) => {
        const dragRect = getDragRect(e);
        if (!dragRect) return;
        drawDragBox(dragRect);
    };

    const onMouseUp = (e: MouseEvent) => {
        const dragRect = getDragRect(e);
        if (!dragRect) return;
        subject.next({ parentRect: containerRef.current!.getBoundingClientRect(), rect: dragRect })

        draggingRef.current = false;
        window.removeEventListener("mousemove", onMouseMove as any);
        window.removeEventListener("mouseup", onMouseUp as any);

        const dragBox = containerRef.current?.querySelector(".drag-box") as HTMLElement;
        if (!dragBox) return;
        dragBox.style.display = "none";
    };

    const drawDragBox = (rect: DOMRect) => {
        const dragBox = containerRef.current?.querySelector(".drag-box") as HTMLElement;
        if (!dragBox) return;
        dragBox.style.left = rect.x + "px";
        dragBox.style.top = rect.y + "px";
        dragBox.style.width = rect.width + "px";
        dragBox.style.height = rect.height + "px";
        dragBox.style.display = "block";
    };

    return (
        <div ref={containerRef} style={{ position: "relative", width: "100%", height: "100%" }}>
            <div className="drag-box" style={{ display: "none", position: "absolute", border: "1.5px dotted red", background: "rgba(255,0,0,0.15)", borderRadius: 3, zIndex: 999 }} />
            <div onMouseDown={onMouseDown} style={{ width: "100%", height: "100%" }}>
                <DragBoxContext.Provider value={subject}>
                    {children}
                </DragBoxContext.Provider>
            </div>
        </div>
    );
};
<!-- Example -->
<DragBox>
    <SelectableItem />
</DragBox>



export function SelectableItem(props: { }) {
    const elementRef = useRef<HTMLDivElement>(null);
    const isSelected = useDragSelect(elementRef, readOnly);
	
    return (
        <div ref={elementRef} className={joinClasses(isSelected ? styles.selected : undefined)} onMouseDown={(e) => e.stopPropagation()}>
            <!-- content -->
        </div>
    );
}

export const joinClasses = (...args: any[]) => Array.from(args).filter(x => !!x).join(" ");


export function clearTextSelection() {
    const w = window as any;
    const d = document as any;
    if (w.getSelection) {
        if (w.getSelection().empty) {  // Chrome
            w.getSelection().empty();
        } else if (w.getSelection().removeAllRanges) {  // Firefox
            w.getSelection().removeAllRanges();
        }
    } else if (d.selection) {  // IE?
        d.selection.empty();
    }
}
83370cookie-checkReact DragBox