Search code examples
javascriptsvgd3.jsdrag

create and drag in same click


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,


Solution

  • 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>