javascript 在React中构建连接节点的可编辑树

k97glaaz  于 2023-01-04  发布在  Java
关注(0)|答案(1)|浏览(167)

我试图在React中构建一个树,其中可以添加节点,连接它们并移动它们,就像包含的图片一样。

一个人该如何着手做这件事?
我正在使用create-react-app
我试过各种各样的方法,但我一直在使用event.clientX/event.pageX时遇到麻烦,它一直给我随机值,导致节点 Flink 。
对我来说,在树的层次上拥有节点移动功能是最有意义的,但是这种方法会带来 Flink 问题。

如何防止event.clientX提供随机值?

我通常遇到的问题是这种codepen中的 Flink :https://codesandbox.io/s/delicate-http-nnzx4?file=/src/App.js(单击并拖动)
我已经尝试了很多事情,它的唯一工作方式(虽然有缺陷)是下面的方式,其中节点移动功能是在节点的水平:

import React, {useState,useEffect,useRef, useCallback, createRef} from 'react';
import "./PrinciplesTree.css"

function Line(props){

    function clickhandler(e){
        e.stopPropagation()
        props.deletenodeconnection(props.firstpoint.node_number,props.secondpoint.node_number)
    }

        const firstpoint = props.firstpoint
        const secondpoint = props.secondpoint

        var x1 = firstpoint.anchor_pos.anchorposx
        var y1 = firstpoint.anchor_pos.anchorposy
        var x2 = secondpoint.anchor_pos.anchorposx
        var y2 = secondpoint.anchor_pos.anchorposy

        if (x2 < x1) {
            var tmp;
            tmp = x2 ; x2 = x1 ; x1 = tmp;
            tmp = y2 ; y2 = y1 ; y1 = tmp;
        }
    
        var lineLength = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
        var m = (y2 - y1) / (x2 - x1);
    
        var degree = Math.atan(m) * 180 / Math.PI;

        const divstyle =  {transformOrigin: 'top left', transform: 'rotate(' + degree + 'deg)', width: lineLength + "px", height: 1 + 'px', background: 'black', 
                            position: 'absolute', top: y1 + "px", left: x1 + "px"}

    return <div className='line' style={divstyle} onClick={clickhandler}></div>
}

function Node(props) {

      const [val, setval] = useState("Enter Principle");
      const node_number = props.nodeN
      const node_width = '150px'
      const anchorel = useRef(null)

      var offsetx = 0
      var offsety = 0

      let parentleft = 0
      let parentright = 0
      let parenttop = 0
      let parentbottom = 0

      const onclick = e =>{
        e.stopPropagation();

        const anchorpositionX = anchorel.current.getBoundingClientRect().left
        const anchorpositionY = anchorel.current.getBoundingClientRect().top

        const parentleft = e.target.parentElement.getBoundingClientRect().left
        const parenttop = e.target.parentElement.getBoundingClientRect().top

        const anchorpos = {anchorposx: anchorpositionX - parentleft, anchorposy: anchorpositionY - parenttop}

        props.connectnode.current(node_number,anchorpos)
      }

      const movehandler = e => {

            var newvalx = e.clientX-parentleft-offsetx
            var newvaly = e.clientY-parenttop-offsety
    
            if(((parentleft + newvalx) < parentright + 5 && (parentleft + newvalx) > parentleft - 5) && 
            (parenttop + newvaly > parenttop - 5 && parenttop + newvaly < parentbottom + 5)){
                props.updatenode(node_number,newvalx,newvaly)
            }
    
        
      }

     const addmovehandler = e => {

        
        const parent = e.target.parentElement

         parentleft = parent.getBoundingClientRect().left
         parentright = parent.getBoundingClientRect().right
         parenttop = parent.getBoundingClientRect().top
         parentbottom = parent.getBoundingClientRect().bottom

        offsetx = e.clientX - e.target.getBoundingClientRect().left
        offsety = e.clientY - e.target.getBoundingClientRect().top

        document.addEventListener('mouseover',movehandler)

        
     } 

     const removenodehandler = e =>{
        const parent = e.target.parentElement
        
        document.removeEventListener('mouseover',movehandler)
     }

    function edit(e){
        e.stopPropagation();
        if(e.key === 'Enter'){
            setval(e.target.value)
        }

    }

    return <div className='node' style = {{left: props.posX, top: props.posY, width: node_width}} onMouseDown={addmovehandler} onMouseUp = {removenodehandler} onClick = {onclick}>
        <div className='anchor' ref={anchorel}></div>
        <textarea className = 'principle' name = {val} onKeyDown={edit} placeholder = {val}></textarea>
        <img src="cross.png" className='Cross' onClick={(e) => props.deletenode(node_number)}></img>
        </div>

}

function PrinciplesTree() {

    const [nodes, setnodes] = useState([]);
    const [connectednodes, setconnectednodes] = useState([]);

    const nodetoconnect = useRef(null)
    const connectnoderef = useRef() 

    useEffect(() => {

        setnodes([{key: 1, nodeN: 1, posX: 0, posY: 0, deletenode: deletenode, connectnode: connectnoderef, updatenode: updatenode}])
      },[]);
    

    const connectnode =  (nodeN,anchorpos)  => {
        
        if(nodetoconnect.current == null){
           nodetoconnect.current = {node_number: nodeN, anchor_pos: anchorpos}
        }else if(nodetoconnect.current != null && nodeN != nodetoconnect.current.node_number )
        {
            const node_to_add = nodetoconnect.current

            const firstnodenumber = nodetoconnect.current.node_number
            const secondnodenumber = nodeN

            var foundpair = false

            connectednodes.forEach(connectednode => {
                const firstnode = connectednode.first.node_number
                const secondnode = connectednode.second.node_number
                
                if((firstnode == firstnodenumber && secondnode == secondnodenumber) || (firstnode == secondnodenumber && secondnode == firstnodenumber)){
                    foundpair = true
                }

            })

            const newnodetoconnect = {first: node_to_add, second: {node_number: nodeN, anchor_pos: anchorpos}}

            if(foundpair == false){
                setconnectednodes(connectednodes => [...connectednodes,newnodetoconnect])
            }

            nodetoconnect.current = null
        }

    }

    connectnoderef.current = connectnode
    
    function deletenodeconnection(node1,node2){
        setconnectednodes(prevconnectednodes => {
            return prevconnectednodes.filter(connectednodes => !(connectednodes.first.node_number == node1 && connectednodes.second.node_number == node2))
        })       
    }

    const deletenode = (NodeN) =>{
        setnodes(prevnodes => {
            return prevnodes.filter(node => node.nodeN !== NodeN)})
    }

    const updatenode = (NodeN,newposx,newposy)=> {

        const updnode = {key: NodeN, nodeN: NodeN,  posX: newposx, posY: newposy, deletenode: deletenode, connectnode: connectnoderef, updatenode: updatenode}

        setnodes(nodes => (
            nodes.map(node => {
            if(node.nodeN == NodeN){
                return updnode
            }
            else return node }
            )))

    }

    function createnode(e){
        
        var el = e.target
        var posX=e.clientX-el.getBoundingClientRect().left
        var posY=e.clientY-el.getBoundingClientRect().top

        var newkey = 0;

       nodes.forEach(node => {
        if(node.key >= newkey){
            newkey = parseInt(node.key) + 1
        }
       });

        var newnode = {key: newkey, nodeN: nodes.length + 1, posX: posX, posY: posY, deletenode: deletenode, connectnode: connectnoderef, updatenode: updatenode}
        setnodes(nodes => [...nodes, newnode]);

    }
    
    return <div onClick={createnode} className='TreeCanvas'>
        {connectednodes.map(connectednode=> <Line firstpoint = {connectednode.first} secondpoint = {connectednode.second} deletenodeconnection={deletenodeconnection}/>)}
        {nodes.map(node => <Node key = {node.key} nodeN = {node.nodeN} posX = {node.posX} posY = {node.posY} deletenode = {node.deletenode} 
                                                                                                            connectnode = {node.connectnode} updatenode = {node.updatenode}/>)}
        </div>
}

export default PrinciplesTree;
bbuxkriu

bbuxkriu1#

一个网名为@wordswithjosh的人在Reddit上帮了我很大的忙,所以我想把他的答案贴在这里,以供将来有同样问题的人参考。

找到了--我在过去让可拖动组件平滑更新时遇到了一些困难。我发现最成功的地方通常是抑制住在组件中使用onDrag或onMouseMove事件的冲动,而不仅仅是使用鼠标移动事件来记住光标的位置,而是使用requestAnimationFrame来实际可视化地移动组件。
这看起来有点过头了,但是当您希望多个组件同时进行可视化更新时,我发现最可靠的模式如下所示:

const TreeNode = () => {
  const [originalLeft, setOriginalLeft] = useState(0); // very rarely is an initial value of 'undefined' desirable; this is one of those times
  const [originalTop, setOriginalTop] = useState(0);
  const [left, setLeft] = useState();
  const [top, setTop] = useState();
  const [originalMouseX, setOriginalMouseX] = useState(0);
  const [originalMouseY, setOriginalMouseY] = useState(0);
  const [newMouseX, setNewMouseX] = useState(0);
  const [newMouseY, setNewMouseY] = useState(0);

  const mutableFrameRef = useRef({ paused: true, lastFrame: null });
  const selfRef = useRef(null);

  const loop = () => {
    // this shouldn't be necessary, but it's a failsafe to prevent
    // runaway recursive function behavior I've experienced in the
    // past when working with rAF
    if (mutableFrameRef.current.paused) return;

    // on every frame, set the new position of the div to its
    // previous position plus the current offset of the mouse
    setLeft(originalLeft + (newMouseX - originalMouseX));
    setTop(originalY + (newMouseY - originalMouseY));

    // this IS necessary, and is the default way of keeping a reference
    // to the handle used to cancel the last requested frame
    mutableFrameRef.current.lastFrame = requestAnimationFrame(loop);
  };

  // not destructuring these params for performance
  const handleMouseMove = e => {
    setNewMouseX(e.clientX);
    setNewMouseY(e.clientY);
  };

  const handleMouseDown = ({ clientX, clientY }) => {
    setOriginalMouseX(clientX);
    setOriginalMouseY(clientY);
    setNewMouseX(clientX);
    setNewMouseY(clientY);
    document.addEventListener('mousemove', handleMouseMove);
  };

  const handleMouseUp = ({ clientX, clientY }) => {
    mutableFrameRef.paused = true;
    cancelAnimationFrame(mutableFrameRef.current.lastFrame);
    // one more time just to be sure
    setLeft(originalLeft + (newMouseX - originalMouseX));
    setTop(originalY + (newMouseY - originalMouseY));

    document.removeEventListener('mousemove', handleMouseMove);
  };

  useEffect(() => {
    // default both our original and "live" top left corner coordinates to what they are on first paint
    const { x, y } = selfRef.current.getBoundingClientRect();
    setOriginalLeft(x);
    setOriginalTop(y);
    setLeft(x);
    setTop(y);
  }, []);

  return (
    <div
      ref={selfRef}
      className='tree-node'
      style={{
        top,
        left
      }}
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      draggable
    >
      // text stuff, tree node contents, etc - not relevant    
    </div>
  );
};

完全公开,我还没有测试过这个确切的配置,我有点像是现场写😅的,但我在过去几乎用过这个确切的模式,我想你可以明白这个想法-主要的是,我们只要求浏览器在每次显示器刷新时重新绘制div,这可以大大提高性能,并帮助消除奇怪的 Flink 。
当然,通过使用onDrag事件更新保存的光标位置,然后使用onDragEnd更新实际绘制的div位置,可以完全避免重新绘制div,但我认为内置HTML拖动过程中显示的“重影”行为并不是您想要的,也不会提供几乎一样漂亮的用户体验。

相关问题