Implement a Flowchart Editor using jsPlumb – Part 2

Hi all, today in my blog I am going to discuss how to make the elements in the palette draggable and the drawing area droppable so that a user can drage elements from the palette and drop the to the drawing area. I discussed implementing the palette, the drawing area and the basic elements in the flowchart editor in my previous blog here. This is the second part of that blog. If you have not read it, please go through it before reading this blog to avoid confusions.

Also note that, I implemented a flowchart diagram editor for WSO2 Process Center (Product-PC) during my internship training period and this tutorial is based on that editor.

To do this, we need the following JavaScript libraries.

First we will define the endpoints and connector styles using jsPlumb.js. You can give two different styles for the source endpoints and target endpoints using jsPlumb thereby distinguishing those two. For convinence, I am using the same styles provided in the jsPlumb demo flowchart editor.

To do this we have to create a new JavaScript file. In our directory structure within the js folder create a new .js file named “flowchart.js”. This file will have all the js functions we are going to implement.

Insert the following code to the flowchart.js file.

var elementCount = 0;
var jsPlumbInstance; //the jsPlumb jsPlumbInstance
jsPlumb.ready(function () {

    jsPlumbInstance = window.jsp = jsPlumb.getInstance({
        // default drag options
        DragOptions: {
            cursor: 'pointer',
            zIndex: 2000
        },
        //the arrow overlay for the connection
        ConnectionOverlays: [
            ["Arrow", {
                location: 1,
                visible: true,
                id: "ARROW";
            }]
        ],
        Container: "canvas";
    });

    //define basic connection type
    var basicType = {
        connector: "StateMachine",
        paintStyle: {strokeStyle:"#216477", lineWidth: 4},
        hoverPaintStyle: {strokeStyle: "blue"}
    };
    jsPlumbInstance.registerConnectionType("basic", basicType);

    //style for the connector
    var connectorPaintStyle = {
        lineWidth: 4,
        strokeStyle: "#61B7CF",
        joinstyle: "round",
        outlineColor: "white",
        outlineWidth: 2
    },
    //style for the connector hover
        connectorHoverStyle = {
            lineWidth: 4,
            strokeStyle: "#216477",
            outlineWidth: 2,
            outlineColor: "white"
        },
        endpointHoverStyle = {
            fillStyle: "#216477",
            strokeStyle: "#216477"
        },

    //the source endpoint definition from which a connection can be started
        sourceEndpoint = {
            endpoint: "Dot",
            paintStyle: {
                strokeStyle: "#7AB02C",
                fillStyle: "transparent",
                radius: 7,
                lineWidth: 3
            },
            isSource: true,
            connector: ["Flowchart", {stub: [40, 60], gap: 5, cornerRadius: 5, alwaysRespectStubs: true}],
            connectorStyle: connectorPaintStyle,
            hoverPaintStyle: endpointHoverStyle,
            connectorHoverStyle: connectorHoverStyle,
            EndpointOverlays: [],
            maxConnections: -1,
            dragOptions: {},
            connectorOverlays: [
                ["Arrow", {
                    location: 1,
                    visible: true,
                    id: "ARROW",
                    direction: 1
                }]
            ]
        },

    //definition of the target endpoint the connector would end
    targetEndpoint = {
        endpoint: "Dot",
        paintStyle: {fillStyle: "#7AB02C", radius: 9},
        maxConnections: -1,
        dropOptions: {hoverClass: "hover", activeClass: "active"},
        hoverPaintStyle: endpointHoverStyle,
        isTarget: true
    };

endpoints

Figure 1 – The source (green hollow) and target (green filled) endpoints

As you can see, I have used almost the same endpoints and connector styles provided in the flowchart demo. I have done some minor changes like removing the label from the connector, changing the size of the target endpoint etc.

As we have now define the styles for the endpoints and connectors, now we can make the elements in the palette draggable. Let’s write a basic function to accomplish this.

 

function makeDraggable(id, className, text){
    $(id).draggable({
        helper: function(){
            return $("<div/>",{
                text: text,
                class:className
            });
        },
        stack: ".custom",
        revert: false
    });
}

For this function, we are passing the id, className and the text of the draggable element. The helper function returns a dive similar to what in the palette once the user tries to drag from an element. Using this function, we can make all the elements in the palette draggable except for the decision element.

makeDraggable("#startEv", "window start jsplumb-connected custom", "start");
makeDraggable("#stepEv", "window step jsplumb-connected-step custom", "step");
makeDraggable("#endEv", "window start jsplumb-connected-end custom", "end");

Note: You can see a new class “custom” in the above code. Define this in the flowchart.css file as following.

.custom{
    position: absolute;
}

The purpose of defining such class is to distinguish the elements in the palette and the drawing area.

The decision element was created by rotating the step element, hence we have to put extra effort to keep the text within this element without rotating. Hence, I will discuss how we can make the decision element draggable later in this post once I implement the required functionalities to accomplish that.

Once you enter the above code in the flowchart.js file, you should be able to drag elements (except the decision element). If the dragged element goes behind the drawing area that can be avoid by giving the z-index. The element having the largest z-index will be shown above the elements having smaller z-index values.

.custom{
    position: absolute;
    z-index: 5;
}

#canvas{
    z-index: 1;
}

Now we have to make the drawing area droppable to put the dragged elements. To do this, insert the following code to the flowchart.js file. (This method will be changed later to add endpoints when dropping on the canvas)

$("#canvas").droppable({
     accept: ".window",
     drop: function (event, ui) {
         ui.helper.clone().appendTo(this);
     }
});

Now you have a flowchart editor in which you can drag elements from the palette and drop them to the drawing area.

To make the decision element draggable, we have to implement another method called “createElement(id)” which will create an element given the id. How can we identify which element to create?

We can identify the element that should be created by using a mousedown event. Hence we have to create a mousedown event for each and every item in the palette. What we can do is, in this mousedown event, we can store the required properties to create the element. These properties inlucde;

  • class name
  • left position
  • top position
  • label value
  • source endpoint locations
  • target endpoint locations
  • whether the label content is editable or not.

To store these properties, we can implement a new method “loadProperties()” as follows:

var properties;
function loadProperties(clsName, left, top, label, startpoints, endpoints, contenteditable) {
    properties = [];
    properties.push({
        left: left,
        top: top,
        clsName: clsName,
        label: label,
        startpoints: startpoints,
        endpoints: endpoints,
        contenteditable: contenteditable
    });
}

Then, for each mousedown even of the elements, we can store these properties corresponding to each element.

 

Var clicked = false;
$('#startEv').mousedown(function () {
    loadProperties("window start custom jtk-node jsplumb-connected", "5em", "5em", "start", ["BottomCenter"],[], false);
    clicked = true;
});
//load properties of a step element once the step element in the palette is clicked
$('#stepEv').mousedown(function () {
    loadProperties("window step custom jtk-node jsplumb-connected-step", "5em", "5em", "step",["BottomCenter"], ["TopCenter"], true);
    clicked = true;
});

//load properties of a decision element once the decision element in the palette is clicked
$('#descEv').mousedown(function () {
    loadProperties("window diamond custom jtk-node jsplumb-connected-step", "5em", "5em", "decision",["LeftMiddle", "RightMiddle", "BottomCenter"], ["TopCenter"], true, 100, 100);
    clicked = true;
});
//load properties of a end element once the end element in the palette is clicked
$('#endEv').mousedown(function () {
    loadProperties("window end custom jtk-node jsplumb-connected-end", "5em", "5em", "end",[], ["TopCenter"], false);
    clicked = true;
});

Note that the source and target endpoint locations are given in an array which is later used by jsPlumb to add these endpoints. The location labels such as “TopCenter”, “RightMiddle” are provided by jsPlumb.js.

In the meantime, I am keeping tabs on which element was clicked using a boolean variable called “clicked”.

So far, when the user tries to drag an element from the palette the corresponding properties of that element will be stored in an array. Using these properties, we can create a DOM element which will be appended on the droppable canvas.

//create an element to be drawn on the canvas
function createElement(id) {
    var elm = $('<div>').addClass(properties[0].clsName).attr('id', id);
    if (properties[0].clsName.indexOf("diamond") > -1) {
        elm.outerWidth("100px");
        elm.outerHeight("100px");
    }
    elm.css({
        'top': properties[0].top,
        'left': properties[0].left
    });

    var strong = $('<strong>');
    if (properties[0].clsName == "window diamond custom jtk-node jsplumb-connected-step") {
        elm.append("<i style='display: none; margin-left: -5px; margin-top: -50px' " +
        "class=\"fa fa-trash fa-lg close-icon desc-text\"><\/i>");
        var p = "<p style='line-height: 110%; margin-top: 25px' class='desc-text' contenteditable='true' " +
            "ondblclick='$(this).focus();'>" + properties[0].label + "</p>";
        strong.append(p);
    }
    else if (properties[0].clsName == "window parallelogram step custom jtk-node jsplumb-connected-step") {
        elm.append("<i style='display: none' class=\"fa fa-trash fa-lg close-icon input-text\"><\/i>");
        var p = "<p style='line-height: 110%; margin-top: 25px' class='input-text' contenteditable='true' " +
            "ondblclick='$(this).focus();'>" + properties[0].label
            + "</p>";
        strong.append(p);
    }
    else if (properties[0].contenteditable) {
        elm.append("<i style='display: none' class=\"fa fa-trash fa-lg close-icon\"><\/i>");
        var p = "<p style='line-height: 110%; margin-top: 25px' contenteditable='true' " +
            "ondblclick='$(this).focus();'>" + properties[0].label + "</p>";
        strong.append(p);
    } else {
        elm.append("<i style='display: none' class=\"fa fa-trash fa-lg close-icon\"><\/i>");
        var p = $('<p>').text(properties[0].label);
        strong.append(p);
    }
    elm.append(strong);
    return elm;
}

The create element slightly differs from the element in the palette. We add the endpoints, a delete icon which will be shown once the element on the drawing area is clicked.

delete

Figure 2 – The delete button and the endpoint of the element

Now we can create an element using the stored properties. Since we finished writing this function, we can make the decision element draggable. This was postponed because the decision element was created by rotating the step element. Hence we can not directly create a div element and set the class name and text as we did for the other elements. To make it draggable,

$("#descEv").draggable({
    helper: function () {
	   return createElement("");
    },
    stack: ".custom",
    revert: false
});


Then we have to draw that element on the drawing area. What we do simply is append the created element to the canvas. So let's define the following function to draw the element.


function drawElement(element, canvasId, name) {
    $(canvasId).append(element);
    _addEndpoints(name, properties[0].startpoints, properties[0].endpoints);
    jsPlumbInstance.draggable(jsPlumbInstance.getSelector(".jtk-node"), {
        grid: [20, 20]
    });
}

Once we append the element on the canvas, we can make it draggable using the jsPlumb draggable function. The grid property is to identify the area in which the element can be dragged.

Now we need to add the endpoints for the element. We have to write a function for this using the functionalities provided by jsPlumb.

//add the endpoints for the elements
var ep;
var _addEndpoints = function (toId, sourceAnchors, targetAnchors) {
    for (var i = 0; i < sourceAnchors.length; i++) {
        var sourceUUID = toId + sourceAnchors[i];
        ep = jsPlumbInstance.addEndpoint("flowchart" + toId, sourceEndpoint, {
            anchor: sourceAnchors[i], uuid: sourceUUID
        });
        sourcepointList.push(["flowchart" + toId, ep]);
        ep.canvas.setAttribute("title", "Drag a connection from here");
        ep = null;
    }
    for (var j = 0; j < targetAnchors.length; j++) {
        var targetUUID = toId + targetAnchors[j];
        ep = jsPlumbInstance.addEndpoint("flowchart" + toId, targetEndpoint, {
            anchor: targetAnchors[j], uuid: targetUUID
        });
        endpointList.push(["flowchart" + toId, ep]);
        ep.canvas.setAttribute("title", "Drop a connection here");
        ep = null;
    }
};

In this method, I am saving the endpoints of a corresponding element in an array so that I can later delete them once I delete the element from the diagram. Also, I am setting a tooltip text for the endpoints.

Now we can change the droppable function to add endpoints when we are dropping the element to the drawing area.


$("#canvas").droppable({
    accept: ".window",
    drop: function (event, ui) {
        if (clicked) {
	        clicked = false;
	        elementCount++;
	        var name = "Window" + elementCount;
	        var id = "flowchartWindow" + elementCount;
	        element = createElement(id);
	        drawElement(element, "#canvas", name);
	        element = "";
        }
    }
});

Now you have a complete flowchart diagram editor in which you can drag and drop elements from the palette to the drawing area and which you can connect using jsPlumb arrows!

In my next blog post, I am going to discuss how you can delete elements and save the flowchart diagram to a JSON string so that you can retrieve it later.

Advertisements

One thought on “Implement a Flowchart Editor using jsPlumb – Part 2

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 )

Google+ photo

You are commenting using your Google+ 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 )

Connecting to %s