create and drag in same click

Viewed 67

I would like to create an application like scratch or node-red, with D3.js, by this I mean create some svg elements by clicking on a 'button list' to create an element and then drag them over an area to arrange them.

This idea is working with my code below. I can click to create shapes (svg group). Once created, I can click on them (AGAIN) and drag it over svg area.

But, I want to mimic the behavior of same apps node-red and scratch, by dragging the new svg element with the same click used to create it. Sparing a click, in one word. But I don't know how to start drag behavior programmatically on the element created. Here is my working code.

var svg = d3.select("body").append("svg")
  .attr("width", 1500)
  .attr("height", 800);
addButton(svg, 'ADD');

function addShape(svg, x, y) {
  var dotContainer = svg.append("g")
    .attr("class", "dotContainer")
    .datum({
      x: x,
      y: y
    })
    .attr("transform", function(d) {
      return 'translate(' + d.x + ' ' + d.y + ')';
    })
    .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended));
  var text = dotContainer.append("text")
    .datum({
      x: 20,
      y: 20
    })
    .attr("x", function(d) {
      return d.x;
    })
    .attr("y", function(d) {
      return d.y;
    })
    .text('Title');
  var rectangle = dotContainer.append("rect")
    .attr("width", 200)
    .attr("height", 100)
    .attr("x", 0)
    .attr("y", 0)
    .attr('style', "opacity:1;fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:5;stroke-opacity:1")
    .attr("ry", 8);
  return dotContainer;
}

function dragstarted(d) {
  let xCoord = d3.event.dx - d3.select(this).attr('x')
  let yCoord = d3.event.dy - d3.select(this).attr('y')
}

function dragged(d) {
  d3.select(this).select("text").text(d.x + ';' + d.y);
  d.x += d3.event.dx;
  d.y += d3.event.dy;
  d3.select(this).attr("transform", function(d, i) {
    return "translate(" + [d.x, d.y] + ")"
  });
}

function dragended(d) {
  d3.select(this).attr("transform", function(d, i) {
    return "translate(" + [d.x, d.y] + ")"
  });
}

function addButton(area, title) {
  var group = area.append("g");
  group.append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", 100)
    .attr("height", 50)
    .attr('style', 'fill:rgb(255,0,0);stroke-width:1;stroke:rgb(200,200,200)');
  group.append("text")
    .attr('x', 20)
    .attr('y', 20)
    .text(title);
  group.on('mousedown', function() {
    var grp = addShape(area, 0, 0);

    //START DRAG ON grp HERE ???
  });
}
<script src="https://d3js.org/d3.v5.min.js"></script>

So, my issue is here that I can't figure out how to call dragstarted() from outside of svg group dotContainer, since dragstarted use this and d, which refers to the svg group. Or use a complete other way to achieve this? I am lost here.... Thanks,

2 Answers

Yo could listen for a mousedown on the button used to create the new shape. In the event listener, you create a new shape and create a new mousedown event which you dispatch immediately on the new element. This new mousedown event will trigger the drag behavior, triggering the drag-start listener once and the drag listener continuously until the mouse is raised. This could look like:

select.on("mousedown", function(event,d) {

   // create some new shape:
   var aNewShape = container.append("shape")
     .attr(...)
     ....

   // create a new event and dispatch it on the new shape
   var e = document.createEvent("MouseEvents");
   e.initMouseEvent("mousedown", true,true,window,0,0,0,event.x,event.y)
   aNewShape.node().dispatchEvent(e)

 })

Which could look something like:

var svg = d3.select("body")
  .append("svg")
  .attr("width",400)
  .attr("height", 300);
  
var data = [
  {shape: d3.symbolCross, y: 0, cy: 25, cx: 25},
  {shape: d3.symbolWye, y: 60, cy: 85, cx: 25 },
  {shape: d3.symbolDiamond, y: 120, cy: 145, cx: 25}
]

// Add some buttons:
var g = svg.selectAll("null")
  .data(data)
  .enter()
  .append("g")
  .attr("transform",function(d,i) {
    return "translate("+[0,d.y]+")";
  })

g.append("rect")
  .attr("width", 50)
  .attr("height", 50)
  .attr("fill", "#ddd");
  
g.append("path")
  .attr("d", function(d) { return  d3.symbol().type(d.shape).size(100)(); })
  .attr("transform","translate(25,25)")
  .attr("fill", "#aaa");

// Some sort of drag function
var drag = d3.drag()
  .on("drag", function(event,d) {
    d.x = event.x;
    d.y = event.y;
    d3.select(this).attr("transform", "translate("+[d.x,d.y]+")");
    
  })
  .on("start", function() {
    d3.select(this).transition()
       .attr("fill","steelblue")
       .duration(1000);
  })

// Mouse down event:
g.on("mousedown", function(event,d) {
   
   
   var shape = svg.append("path")
       .datum({type:d.shape,x:d.cx,y:d.cy})
       .attr("d", d3.symbol().type(d.shape).size(300)())
       .attr("transform", function(d) { return "translate("+[d.x,d.y]+")" })
       .attr("fill","black")
       .call(drag);
       
   var e = document.createEvent("MouseEvents");
   e.initMouseEvent("mousedown", true,true,window,0,0,0,event.x,event.y)
   shape.node().dispatchEvent(e);

})
  
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>

When in doubt, you can always reach back to vanilla JavaScript. In this case, you can dispatch a custom MouseDown event using the d3.event object as the attribute dictionary, essentially cloning the element.

Then, the MouseMove events take over and are processed seamlessly:

var svg = d3.select("body").append("svg")
  .attr("width", 1500)
  .attr("height", 800);
addButton(svg, 'ADD');

function addShape(svg, x, y) {
  var dotContainer = svg.append("g")
    .attr("class", "dotContainer")
    .datum({
      x: x,
      y: y
    })
    .attr("transform", function(d) {
      return 'translate(' + d.x + ' ' + d.y + ')';
    })
    .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended));
  var text = dotContainer.append("text")
    .datum({
      x: 20,
      y: 20
    })
    .attr("x", function(d) {
      return d.x;
    })
    .attr("y", function(d) {
      return d.y;
    })
    .text('Title');
  var rectangle = dotContainer.append("rect")
    .attr("width", 200)
    .attr("height", 100)
    .attr("x", 0)
    .attr("y", 0)
    .attr('style', "opacity:1;fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:5;stroke-opacity:1")
    .attr("ry", 8);
  return dotContainer;
}

function dragstarted(d) {
  let xCoord = d3.event.dx - d3.select(this).attr('x')
  let yCoord = d3.event.dy - d3.select(this).attr('y')
}

function dragged(d) {
  d3.select(this).select("text").text(d.x + ';' + d.y);
  d.x += d3.event.dx;
  d.y += d3.event.dy;
  d3.select(this).attr("transform", function(d, i) {
    return "translate(" + [d.x, d.y] + ")"
  });
}

function dragended(d) {
  d3.select(this).attr("transform", function(d, i) {
    return "translate(" + [d.x, d.y] + ")"
  });
}

function addButton(area, title) {
  var group = area.append("g");
  group.append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", 100)
    .attr("height", 50)
    .attr('style', 'fill:rgb(255,0,0);stroke-width:1;stroke:rgb(200,200,200)');
  group.append("text")
    .attr('x', 20)
    .attr('y', 20)
    .text(title);
  group.on('mousedown', function() {
    var grp = addShape(area, 0, 0);

    grp.node().dispatchEvent(new MouseEvent(
      "mousedown",
      d3.event
    ));
  });
}
<script src="https://d3js.org/d3.v5.js"></script>

Related