我试图在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;
1条答案
按热度按时间bbuxkriu1#
一个网名为@wordswithjosh的人在Reddit上帮了我很大的忙,所以我想把他的答案贴在这里,以供将来有同样问题的人参考。
找到了--我在过去让可拖动组件平滑更新时遇到了一些困难。我发现最成功的地方通常是抑制住在组件中使用onDrag或onMouseMove事件的冲动,而不仅仅是使用鼠标移动事件来记住光标的位置,而是使用requestAnimationFrame来实际可视化地移动组件。
这看起来有点过头了,但是当您希望多个组件同时进行可视化更新时,我发现最可靠的模式如下所示:
完全公开,我还没有测试过这个确切的配置,我有点像是现场写😅的,但我在过去几乎用过这个确切的模式,我想你可以明白这个想法-主要的是,我们只要求浏览器在每次显示器刷新时重新绘制div,这可以大大提高性能,并帮助消除奇怪的 Flink 。
当然,通过使用onDrag事件更新保存的光标位置,然后使用onDragEnd更新实际绘制的div位置,可以完全避免重新绘制div,但我认为内置HTML拖动过程中显示的“重影”行为并不是您想要的,也不会提供几乎一样漂亮的用户体验。