HTML, SVG, and Drag/Drop with Raphaël


I’m working on a project where I need to create an SVG image on the fly, based on user input, inside a web page. The image will consist of rectangles with various amounts of text within the rectangle. There will be connector lines between the rectangles. And I need to be able to drag the rectangles around.

If I’m working with rectangles only, this is real simple and any of the tools below can take care of that. But it seems that moving the rectangle AND the text at the same time is a little complex and some of the tools do not work well.

With HTML5 quickly gaining more traction in the industry and it’s ability to embed SVG code directly into the HTML and have it render right, I was hoping this would be a simple problem to solve. I was only partially right.

I took a look at some tools for working with SVG in the current web browsers:

jQuery.SVG looked REAL promising at first. I was able to set things up, and drag around the rectangle and text. BUT, occasionally the x/y position of the object would revert to 0,0 in the middle of dragging the objects. If I were dragging to the left or up when this happened, my objects would disappear (due to the negative x/y values being outside of the visible area) And then there was no way to get it back. I was able to confirm this behavior with other developers who tested my code. The reseting of the x/y values to zero mid drag appears to be a bug in the jQuery.UI library, at least when related to SVG objects being dragged. I never did find a way around this.

So I moved on. In reality, I moved to Raphaël next, but I’ll come back to that one.

SVGWeb looks really promising. However, it looks like it needs a little more effort to set up, and I was able to resolve the issues I had with Rapaël before needing to dig deeper into SVGWeb. I still want to do some work with SVGWeb but need to put that on the back burner for now.

Raphël. Setting up drag/drop with Raphël is pretty simple – call “mySvgObj.drag()” and pass in three function references. The first indicates what to do when things are moving, the second when dragging starts, and the last when dragging ends. BUT, there are some hidden gotchas here.

First, the .drag() function can be applied to a “set” (sort of like an SVG group, but not quite – a set is more of an array of related elements). However, when dragging the referenced element is the first object in the set and not the set itself.

Second, the .drag() appears to not work against text elements directly, though I didn’t spend tooo much time trying to make this work.

And third – applying an SVG transform (move operation) to a set has undesired results. In my case, the rectangle would move, but the text would not.

I’m happy to say, I have beaten these gotchas. And these are not small gotchas – searching the web I found many instances of people trying to do similar work, most without success. So I’m posting my code here to hopefully help out those folks, and make future efforts easier.

The trick is with the sets. You need to stop thinking of a set as an SVG group. Rather, you need to think of the set as an array of SVG elements. With this in mind, we can start to see that instead of applying a transform to the set, or setting the x/y position of it, we need to take action on all of the elements within the set.

Check out the sample page.

Here is the code behind that page. (watch out for the quirks of my syntax highlighter – view the original code by hovering over the text below, then clicking on the first icon in the upper right corner)

001.<script type="text/javascript" src="js/jquery-1.4.2.min.js"></script>
002.<script type="text/javascript" src="js/raphael-min.js"></script>
003.<script type="text/javascript" src="js/raphael.draggable.js"></script>
004.
005.<script type="text/javascript">
006.//variables we'll need throughout the sample code
007.var workspace;
008.var groups = [];     //just an array of the sets we create.
009.
010.//Stuff to do when the page is done loading
011.$(document).ready( function () {
012.redraw();
013.});
014.
015.//The routine to repaint the drawing area
016.function redraw() {
017.workspace = Raphael('workspace', "100%", "80%");
018.
019.//Add a rectangle
020.var rect = workspace.rect(0,0, 150, 100, 3);
021.rect.attr({
022."stroke": "#00f",
023."stroke-width": 2,
024."fill" : "#fff"
025.});
026.
027.var txt =  workspace.text(75, 50, "Drag/Drop\nExample");
028.txt.attr({
029."width" : 150,
030."fill": "#000",
031."font-size": "12pt",
032."font-weight": "bold"
033.});
034.
035.//Create a set so we can move the
036.//text and rectangle at the same time
037.var g = workspace.set(rect, txt);
038.
039.//store the next index in our groups array
040.//so we can easily find the set later
041.//then add the set to the groups array
042.rect.idx = groups.length; 
043.groups.push(g);
044.
045.//set up drag/drop
046.// - This could be applied to the set as well, but the "dragged"
047.//   object ends up being the rect anyways.
048.rect.drag(dragMove, dragStart, dragStop);
049.}
050.
051.//set up our object for dragging
052.function dragStart() {
053.var g = null;
054.if (!isNaN(this.idx)) {
055.//find the set (if possible)
056.var g = groups[this.idx];
057.}
058.if (g) {
059.var i;
060.//store the starting point for each item in the set
061.for(i=0; i < g.items.length; i++) {
062.g.items[i].ox = g.items[i].attr("x");
063.g.items[i].oy = g.items[i].attr("y");
064.}
065.}
066.}
067.
068.//clean up after dragging
069.function dragStop() {
070.var g = null;
071.if (!isNaN(this.idx)) {
072.//find the set (if possible)
073.var g = groups[this.idx];
074.}
075.if (g) {
076.var i;
077.//remove the starting point for each of the objects
078.for(i=0; i < g.items.length; i++) {
079.delete(g.items[i].ox);
080.delete(g.items[i].oy);
081.}
082.}
083.}
084.
085.//take care of moving the objects when dragging
086.function dragMove(dx, dy) {
087.if (!isNaN(this.idx)) {
088.var g = groups[this.idx];
089.}
090.
091.if (g) {
092.var x;
093.//reposition the objects relative to their start position
094.for(x = 0; x < g.items.length; x++) {
095.var obj = g.items[x];   //shorthand
096.obj.attr({ x: obj.ox + dx, y: obj.oy + dy });
097.
098.//optional:  We can do a check here to see what property
099.//           we should be changing.
100.// i.e. (haven't fully tested this yet):
101.// switch (obj.type) {
102.//     case "rect":
103.//     case "text":
104.//         obj.attr({ x: obj.ox + dx, y: obj.oy + dy });
105.//         break;
106.//     case "circle":
107.//         obj.attr({ cx: obj.ox + dx, cy: obj.oy + dy });
108.// }
109.}
110.}
111.}
112.</script>
113.<div id="workspace" style="width: 100%; height: 100%;"></div>

And a quick breakdown of what the code does:

  • First, we include the necessary libraries. As you can see I am also making use of jQuery.
  • Next we set up some global variables we’ll reference throughout the code.
  • Then we have the typical jQuery .ready() function to call the redraw() function when the page is done loading.
  • Inside the Redraw function:
    • We create our workspace and store a reference to it in the “workspace” variable. Notice the ‘workspace’ string – this must match the ID of the DIV where we want our Raphael workspace to be.
    • A rectangle is created.
    • And some attributes are set for the rectangle.
    • A text element is created.
    • And some attributes are set for the text element.
    • We create a Raphael “set” containing the rectangle and text elements.
    • We add our set to the global array of sets (the groups array). We also define an “idx” property on the rectangle and set it to the index in the groups array. This is a bit of a hack/shortcut – it allows us to quickly find what set the dragged rectangle belongs to.
    • And finally, we set up the drag/drop on the rectangle. It should be noted that we could have said “g.drag(…)” as well to apply the drag() function to the group. But, when we actually do the drag, the rectangle is the referenced object, so we may as well just deal with the rectangle in the first place. We pass three function references to the .drag() routine.
  • The dragStart() function :
    • We define a variable “g” that will be used to reference to the set.
    • within the drag functions “this” refers to the element the HTML document object model thinks is executing the function. In our case it is the rectangle. So anytime you see “this.something”, think of it as “current_rectangle.something”.
    • We do a check to see if the current rectangle has an “idx” property, and if so make sure it is a number. We use the isNaN() function here because if we simply did “if (this.idx)” and the idx was equal to zero, then that if block would equate to false – zero is a valid value for us in this particular case.
    • So if we do have an “idx” property, we use it to find out what set we are dealing with. Real simple in this sample code, seeing as there is only one set. But this also works just fine with many different sets.
    • If we have a valid reference to a set, we loop over the items in the set and store the starting position for each of the items. This part is based on the original Drag-n-Drop examples.
  • The dragStop() function:
    • The first bit is identical to that described above in the dragStart() function. We get a reference to the appropriate set if possible.
    • And then we loop over each of the items in the set and remove the starting x/y values.
  • The dragMove() function:
    • The function takes two parameters – dx and dy. These are the change in the x/y position since the drag started.
    • Again, we get a reference to the appropriate set. See the discussion above in the dragStart() function for details.
    • Then we loop over each of the items in the set and change the x/y attributes to be the starting point plus the appropriate change indicated by dx/dy.
    • NOTE: The code as is only works for items that use the x/y properties to set the position. Some elements, like circles, use different properties, like cx/cy. We can easily check what type of object we are dealing with and then set the appropriate properties. In my case, rectangles and text both use x/y so I kept the code simple.
  • And that is it for the JavaScript!
  • In the body, we do need a DIV that has an ID of “workspace”. It is also a really good idea to make sure the DIV is large enough to actually see something. 🙂

And with that, I have enough functionality to continue with my particular project. This approach should also be useful for others working Raphael and needing to drag/drop complex shapes.

For the Raphael experts out there – please feel free to let me know if the above code can be improved.

Copy from  grover.open2space.com/content/html-svg-and-dragdrop-rapha%C3%ABl

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s