我正在构建一个流生成器,并且我将使用火盆曲线在它们之间连接多个可拖动节点。为了将一个节点连接到另一个节点,只需将其悬停在该节点上,单击加符号并将一条线拖到另一个节点即可。将绘制贝塞尔曲线。
我使用通用路径画线,但是在完成拖动事件之后,我想克隆它,并将其存储在 g 组中。问题在于,拖动圆时,新路径不会更新坐标。如何动态地做到这一点?我还要附加一个JSFiddle。为了示例起见,我试图简化代码,它最初是在AngularJs环境中运行的。
"use strict";
const sidebar = document.getElementById('right_sidebar');
const width = document.getElementById('IvrBuilderController').offsetWidth,
height = document.getElementById('IvrBuilderController').offsetHeight;
var xLoc = width / 2 - 25,
yLoc = 100,
radius = 40;
var zoom = d3.zoom()
.scaleExtent([0.7, 5])
.on("zoom", zoomed);
/** MAIN SVG **/
var svg = d3.select("#IvrBuilderController").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom)
;
var state = {
shiftNodeDrag: false,
dragLineOverNode: false,
startDragLineNode: null,
endDragLineNode: null
};
var g = svg.append("g");
var paths_container = g.append("g").attr('id', 'paths_container');
var dragLine = g.append('svg:path')
.attr('class', 'link dragline hidden')
.attr('d', 'M0,0L0,0');
var nodes_container = g.append("g").attr('id', 'nodes_container');
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
}
// starting node
var xLoc = width / 2 - 25,
yLoc = 100;
var nodes = [{ x: xLoc, y: yLoc, id: 0, title: "start" }, { x: 350, y: 250, id: 1, title: "newNode" }, { x: 100, y: 250, id: 2, title: "NewNode" }];
var nodeConnections = [];
function updateNodes() {
const container = nodes_container.selectAll(".node")
.data(nodes)
.enter();
const node = container
.append("g").attr("class", "node")
.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node
.append("circle")
.attr("r", radius)
.attr("class", "circle_node")
.on("click", selectNode)
.on("mouseover", mouseOverNode)
.on("mouseout", mouseOutNode)
node
.append("image")
.attr("xlink:href", "https://i.imgur.com/W02KovJ.png")
.attr("x", -8)
.attr("y", -48)
.attr("width", 16)
.attr("height", 16)
.attr("class", "add_node_icon")
.on("mousedown", (d) => {
// reposition dragged directed edge
dragLine.classed('hidden', false)
.attr('d', 'M' + d.x + ',' + d.y + 'L' + d.x + ',' + d.y);
return;
})
.call(d3.drag()
.on("start", () => {
state.shiftNodeDrag = true;
})
.on("drag", function (d, i) {
dragLine.attr('d', 'M' + d.x + ',' + d.y + 'L' + d3.mouse(g.node())[0] + ',' + d3.mouse(g.node())[1]);
state.startDragLineNode = d;
state.startDragLineNode.el = d3.select(this);
})
.on("end", function (d) {
if (state.dragLineOverNode) {
console.log('connected1');
dragLine
.attr("d", function (d) {
const startX = state.startDragLineNode.x;
const startY = state.startDragLineNode.y - 41;
const endX = state.endDragLineNode.x;
const endY = state.endDragLineNode.y + 41;
const dx = Math.abs(startX - endX) * 0.675;
const p2y = startY - dx / 3;
const p3y = endY + dx / 3;
const data = `M${startX} ${startY} C ${startX} ${p2y} ${endX} ${p3y + 50} ${endX} ${endY}`;
let dragLineClone = paths_container
.append('path')
.attr("class", "link dragline")
.attr("d", data);
nodeConnections.push(dragLineClone);
return data;
});
} else {
dragLine.classed('hidden', true);
}
state.shiftNodeDrag = false;
updateNodes();
})
);
node
.append("image")
.attr("xlink:href", "https://i.imgur.com/W02KovJ.png")
.attr("x", -8)
.attr("y", 32)
.attr("width", 16)
.attr("height", 16)
.attr("class", "add_node_icon")
.on("mousedown", (d) => {
// reposition dragged directed edge
dragLine.classed('hidden', false)
.attr('d', 'M' + d.x + ',' + d.y + 'L' + d.x + ',' + d.y);
return;
})
.call(d3.drag()
.on("start", () => {
state.shiftNodeDrag = true;
})
.on("drag", (d, i) => {
dragLine.attr('d', 'M' + d.x + ',' + d.y + 'L' + d3.mouse(g.node())[0] + ',' + d3.mouse(g.node())[1]);
state.startDragLineNode = d;
})
.on("end", (d) => {
if (state.dragLineOverNode) {
console.log('connected2');
dragLine
.attr("d", function (d) {
const startX = state.startDragLineNode.x;
const startY = state.startDragLineNode.y - 41;
const endX = state.endDragLineNode.x;
const endY = state.endDragLineNode.y + 41;
const dx = Math.abs(startX - endX) * 0.675;
const p2y = startY - dx / 3;
const p3y = endY + dx / 3;
const data = `M${startX} ${startY} C ${startX} ${p2y} ${endX} ${p3y + 50} ${endX} ${endY}`;
return data;
});
} else {
dragLine.classed('hidden', true);
}
state.shiftNodeDrag = false;
})
);
nodeConnections.forEach(element => {
paths_container
.append('path')
.attr("class", "link dragline")
.attr("d", element);
});
}
updateNodes(); // first init
function addNode(title, id, x, y) {
nodes.push({ title: title, id: id, x: x, y: y });
updateNodes();
}
function mouseOverNode(d, i) {
if (state.shiftNodeDrag && (state.startDragLineNode !== d)) {
state.dragLineOverNode = true;
state.endDragLineNode = d;
state.endDragLineNode.el = d3.select(this);
} else {
state.dragLineOverNode = false;
}
}
function mouseOutNode(d, i) {
state.dragLineOverNode = false;
}
function selectNode(d) {
if (!d3.select(this).classed("selected")) {
d3.select(this).raise().classed("selected", true);
sidebar.classList.add("open");
} else {
d3.select(this).raise().classed("selected", false);
sidebar.classList.remove("open");
}
if (d3.event) { if (d3.event.defaultPrevented) return; }
}
function deselectNode() {
d3.select("body").selectAll(".selected").classed("selected", false);
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).raise().classed("dragged", true);
}
function dragged(d) {
if (!state.shiftNodeDrag) {
d.x = d3.event.x;
d.y = d3.event.y;
d3.select(this).attr("transform", "translate(" + d3.event.x + "," + d3.event.y + ")");
}
dragLine
.attr("d", function (d) {
const startX = state.startDragLineNode.x;
const startY = state.startDragLineNode.y - 41;
const endX = state.endDragLineNode.x;
const endY = state.endDragLineNode.y + 41;
const dx = Math.abs(startX - endX) * 0.675;
const p2y = startY - dx / 3;
const p3y = endY + dx / 3;
const data = `M${startX} ${startY} C ${startX} ${p2y} ${endX} ${p3y + 50} ${endX} ${endY}`;
return data;
});
}
function dragended(d) {
d3.select(this).classed("dragged", false);
}
var closeSidebar = function () {
document.getElementById('right_sidebar').classList.remove('open');
deselectNode();
}
function allowDrop(ev) {
ev.preventDefault();
}
function drag(ev) {
ev.dataTransfer.setData("text", ev.target.id);
document.getElementById('drop-zone').style.opacity = 1;
document.getElementById('drop-zone').style.display = 'block';
var child = ev.target;
var parent = child.parentNode;
var index = Array.prototype.indexOf.call(parent.children, child);
ev.dataTransfer.setData("target_id", index);
}
function drop(ev) {
ev.preventDefault();
const data = ev.dataTransfer.getData("text");
const target_id = ev.dataTransfer.getData("target_id");
ev.target.appendChild(document.getElementById(data));
document.getElementById('drop-zone').style.opacity = 0;
document.getElementById('drop-zone').style.display = 'none';
const target = ev.target.firstElementChild;
const toolbox = document.getElementById('toolbox');
toolbox.insertBefore(target, toolbox.children[target_id]); // clone the dragged icon at the same position
let nodeLastIndex;
nodes.forEach(el => {
nodeLastIndex = el.id;
});
addNode(target.attributes.name.nodeValue, nodeLastIndex + 1, ev.layerX, ev.layerY);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #ffffff!important;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
p {
text-align: center;
overflow: overlay;
position: relative;
}
#IvrBuilderController {
position: relative;
height: 100vh;
background-color: #ffffff;
}
.page-header.navbar {
background-color: #16191c !important;
}
.node text {
pointer-events: none;
}
g.node circle {
stroke: #333;
stroke-width: 2px;
}
.circle_node:hover {
stroke: #333;
cursor: pointer;
}
.circle_node {
fill: transparent;
stroke: #333;
}
.circle_node.selected {
stroke: red;
}
.add_node_icon {
stroke: transparent;
cursor: pointer;
display: none;
}
g.node .delete {
transition: stroke .2s;
}
g.node .delete:hover {
stroke: #cf2929;
}
g.node .delete+text {
transition: fill .2s;
}
g.node .delete:hover+text {
fill: #cf2929;
}
g.node .delete:hover {
cursor: pointer;
}
g.node:hover .add_node_icon {
display: block;
}
g.selected circle {
fill: rgb(250, 232, 255);
}
g.selected:hover circle {
fill: rgb(250, 232, 255);
}
path.link {
fill: none;
stroke: #333;
stroke-width: 2px;
cursor: default;
}
path.link:hover {
stroke: rgb(94, 196, 204);
}
g.connect-node circle {
fill: #BEFFFF;
}
path.link.hidden {
stroke-width: 0;
}
path.link.selected {
stroke: rgb(229, 172, 247);
}
.draggable {
position: absolute;
z-index: 9;
background-color: #f1f1f1;
border: 1px solid #d3d3d3;
text-align: center;
width: 150px;
cursor: move;
}
#right_sidebar {
width: 300px;
overflow: hidden;
background: #EEEEEE;
-webkit-transition: right 0.3s;
-moz-transition: right 0.3s;
-ms-transition: right 0.3s;
-o-transition: right 0.3s;
transition: right 0.3s;
position: fixed;
right: -320px;
height: 100vh;
border-left: 1px solid black;
display: flex;
flex-direction: column;
padding: 10px;
}
#right_sidebar.open {
right: 0;
}
#right_sidebar i {
cursor: pointer;
font-size: 20px;
}
#right_sidebar a {
cursor: pointer;
font-size: 20px;
text-transform: uppercase;
color: black;
margin-top: 10px;
}
#drop-zone {
border: 2px solid red;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(255, 255, 255, 0.9);
z-index: 10;
opacity: 0;
display: none;
transition: opacity .2s;
}
#drop-zone:after {
content: 'drop zone';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: grey;
text-transform: uppercase;
}
#drop-zone p {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
font-size: 20px;
color: grey;
}
#IvrBuilderController {
background-image: linear-gradient(0deg, transparent 24%, #f2f2f2 25%, #f2f2f2 26%, transparent 27%, transparent 74%, #f2f2f2 75%, #f2f2f2 76%, transparent 77%, transparent), linear-gradient(90deg, transparent 24%, #f2f2f2 25%, #f2f2f2 26%, transparent 27%, transparent 74%, #f2f2f2 75%, #f2f2f2 76%, transparent 77%, transparent);
background-size: 50px 50px;
}
svg {
height: 100%;
}
.error {
/* display: none; */
color: red;
margin: 5px 0;
text-align: left;
}
.muteButton {
display: none;
}
.zoom_container {
z-index: 2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.min.js"></script>
<div id="IvrBuilderController">
<div id="drop-zone" ondrop="drop(event)" ondragover="allowDrop(event)"></div>
<div id="toolbox">
</div>
<div id="right_sidebar">
<i class="fa fa-times" aria-hidden="true" ng-click="closeSidebar()"></i>
<ul>
<li><a href="#" id="deleteNode">Delete</a></li>
<li><a href="#" id="editNodeName">Edit name</a></li>
</ul>
</div>
</div>