reactjskonvajsreact-konva

React-konva multiple select element


import React, {useEffect, useRef, useState} from "react";
import {Circle, Layer, Rect, Stage, Transformer} from "react-konva";
import Konva from "konva";

const App = () => {
    const [rectPosition, setRectPosition] = useState({x: 50, y: 50});
    const [circlePosition, setCirclePosition] = useState({x: 150, y: 150});
    const [selectedIds, setSelectedIds] = useState([]);
    const [isDragging, setIsDragging] = useState(false);
    const stageRef = useRef(null);
    const layerRef = useRef(null);
    const transformerRef = useRef();
    const selectionRectRef = useRef();

    const selection = useRef({
        visible: false,
        x1: 0,
        y1: 0,
        x2: 0,
        y2: 0,
    });

    const MIN_SCALE = 0.5;
    const MAX_SCALE = 2;
    
    
    const handleWheel = (e) => {
        e.evt.preventDefault();
        const stage = stageRef.current;
        const oldScale = stage.scaleX();
        const pointer = stage.getPointerPosition();
        const scaleBy = 1.05;
        let newScale = e.evt.deltaY > 0 ? oldScale * scaleBy : oldScale / scaleBy;
        newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, newScale));

        stage.scale({x: newScale, y: newScale});

        const mousePointTo = {
            x: (pointer.x - stage.x()) / oldScale,
            y: (pointer.y - stage.y()) / oldScale,
        };
        const newPos = {
            x: pointer.x - mousePointTo.x * newScale,
            y: pointer.y - mousePointTo.y * newScale,
        };

        stage.position(newPos);
        stage.batchDraw();
    };


    const updateSelectionRect = () => {
        const node = selectionRectRef.current;
        const stage = stageRef.current;
        const scaleX = stage.scaleX();
        const scaleY = stage.scaleY();
        const pos = stage.position();

        node.setAttrs({
            visible: selection.current.visible,
            x: (Math.min(selection.current.x1, selection.current.x2) - pos.x) / scaleX,
            y: (Math.min(selection.current.y1, selection.current.y2) - pos.y) / scaleY,
            width: Math.abs(selection.current.x1 - selection.current.x2) / scaleX,
            height: Math.abs(selection.current.y1 - selection.current.y2) / scaleY,
            fill: "rgba(0, 161, 255, 0.3)",
        });
        node.getLayer().batchDraw();
    };

    const onMouseDown = (e) => {
        const isTransformer = e.target.findAncestor("Transformer");
        if (isTransformer) {
            return;
        }

        if (e.evt.shiftKey) {
            const pos = e.target.getStage().getPointerPosition();
            selection.current.visible = true;
            selection.current.x1 = pos.x;
            selection.current.y1 = pos.y;
            selection.current.x2 = pos.x;
            selection.current.y2 = pos.y;
            updateSelectionRect();
            setIsDragging(false); 
        } else {
            setIsDragging(true); 
        }
    };

    const onMouseMove = (e) => {
        if (!selection.current.visible) return;

        const pos = e.target.getStage().getPointerPosition();
        selection.current.x2 = pos.x;
        selection.current.y2 = pos.y;
        updateSelectionRect();
    };

    const onMouseUp = () => {
        const selBox = selectionRectRef.current.getClientRect();

        console.log("Selection box coordinates:", selBox); 

        const elements = [];

        layerRef.current.find(".selectable").forEach((elementNode) => {
            const elBox = elementNode.getClientRect();
            console.log(`Element ${elementNode.id()} box coordinates:`, elBox); 

            if (Konva.Util.haveIntersection(selBox, elBox)) {
                console.log(`Element ${elementNode.id()} intersects with selection box.`); 
                elements.push(elementNode);
            }
        });

        if (elements.length > 0) {
            console.log("Selected elements:", elements.map(el => el.id())); 
        } else {
            console.log("No elements intersect with the selection box."); 
        }

        setSelectedIds(elements.map((el) => el.id())); 
        transformerRef.current.nodes(elements); 
        selection.current.visible = false;
        updateSelectionRect();
    };


    const onClickTap = (e) => {
        const isShiftPressed = e.evt.shiftKey; 
        if (selectionRectRef.current.visible()) {
            return; 
        }

        const clickedOnEmpty = e.target === e.target.getStage();
        if (clickedOnEmpty) {
            setSelectedIds([]);
            return;
        }

        const shape = e.target;
        const isSelected = selectedIds.includes(shape.id());

        if (isSelected) {
            setSelectedIds((prevSelected) =>
                prevSelected.filter((id) => id !== shape.id())
            );
        } else {
            setSelectedIds((prevSelected) => [...prevSelected, shape.id()]);
        }
    };

    useEffect(() => {
        const transformer = transformerRef.current;
        const nodes = selectedIds.map((id) => layerRef.current.findOne("#" + id));

        if (nodes.length > 0) {
            transformer.nodes(nodes);
            transformer.getLayer().batchDraw();
        } else {
            transformer.nodes([]);
        }
    }, [selectedIds]);

    return (
        <div>
            <Stage
                ref={stageRef}
                width={window.innerWidth}
                height={window.innerHeight}
                draggable={isDragging}
                onWheel={handleWheel}
                onMouseDown={onMouseDown}
                onMouseMove={onMouseMove}
                onMouseUp={onMouseUp}
                onClick={onClickTap}
            >
                <Layer ref={layerRef}>
                    <Rect
                        id="rect1"
                        className="selectable"
                        x={rectPosition.x}
                        y={rectPosition.y}
                        width={100}
                        height={100}
                        fill="blue"
                        draggable
                        onDragEnd={(e) => setRectPosition({x: e.target.x(), y: e.target.y()})}
                    />
                    <Circle
                        id="circle1"
                        className="selectable"
                        x={circlePosition.x}
                        y={circlePosition.y}
                        radius={50}
                        fill="green"
                        draggable
                        onDragEnd={(e) => setCirclePosition({x: e.target.x(), y: e.target.y()})}
                    />
                    <Transformer ref={transformerRef}/>
                    <Rect ref={selectionRectRef}/>
                </Layer>
            </Stage>
        </div>
    );
};

export default App;

I started creating an application based on react-konva and after implementing a few basic features like drag and drop moving and zoom, I started creating a mechanism to select multiple items at the same time, but my selection area cannot catch any item when it is selected (onMouseUp) and I don't know what is wrong.


Solution

  • You are setting

    className="selectable"
    

    But you should use name, so

    name="selectable"
    

    The Konva.Stage.find() function uses the name attribute.

    See the documentation for find here.