script.js | |
---|---|
This script shows an example for integrating the Raphael and Knockout libraries. | (function() {
var Graph, View, helpers; |
Event handlers for GUI actionsSet up handlers for events fired via the eve library bundled with Raphael.js, returning a function that sets the current action | function initActions()
{
var currentAction = 'noop', actions; |
First level: selected action | actions = {
'create-node': {
'paper-clicked': function() { this.createNode(); }
},
'delete-node': {
'node-clicked': function(node) { this.deleteNode(node); }
},
'create-edge': {
'node-dnd-node': function(from, to) { this.connect(from, to); }
},
'delete-edge': {
'edge-clicked': function(edge) { this.deleteEdge(edge); }
}
}; |
Bind to all graph events, call appropriate handler if exists | eve.on('graph.*', function() {
console.log("Controller received event " + eve.nt(), this, arguments);
var parts = eve.nt().split('.');
if (typeof(actions[currentAction]) != 'undefined' &&
typeof(actions[currentAction][parts[1]]) != 'undefined') {
actions[currentAction][parts[1]].apply(this, arguments);
}
}); |
The function to set the action | return function(a) {
console.log("Action set to " + a);
currentAction = a;
}
}; |
Graph model | Graph = (function() {
var Node, Edge, Constructor, gid = 0, eid = 0, nid = 0; |
Simple node model. Note that label and color are observables, while the edges will be observable on the graph model. You'll probably want to have a list of in-edges and out-edges in real uses. | Node = function Node(graph)
{
this.graph = graph;
this.type = 'node';
this.label = ko.observable('N' + (nid++));
this.color = ko.observable('red');
}; |
Simple edge model. Note again that label and color are observables. | Edge = function Edge(graph, from, to)
{
this.graph = graph;
this.type = 'edge';
this.label = ko.observable('E' + (eid++));
this.color = ko.observable('black');
this.from = from;
this.to = to;
}; |
Graph model. This is a bit more complex. The node and edge lists are | Constructor = (function() {
var Graph = function Graph() {
this.id = gid++;
this.nodes = ko.observableArray();
this.edges = ko.observableArray();
};
Graph.prototype.createNode = function() {
var node = new Node(this);
this.nodes.push(node);
return node;
};
Graph.prototype.deleteNode = function(node) {
var i, edges, edge;
node.graph = null;
this.nodes.remove(node);
edges = this.edges();
for (i in edges) {
edge = ko.utils.unwrapObservable(edges[i]);
console.log(i, edge, node);
if (edge.from == node || edge.to == node) {
this.deleteEdge(edge);
}
}
};
Graph.prototype.connect = function(from, to) {
var edge = new Edge(this, from, to);
this.edges.push(edge);
return edge;
};
Graph.prototype.deleteEdge = function(edge) {
edge.graph = null;
this.edges.remove(edge);
};
return Graph;
})();
return Constructor;
})(); |
Custom Knockout binding handlers | |
element.attrCreate a Knockout binding handler calling the Raphael
The handler returned by this function accepts a hash as argument.
The Values can be KO observables (this is the point of the whole thing) | function createRaphaelAttrBindingHandler(paper)
{
return {
update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
var dict = ko.utils.unwrapObservable(valueAccessor()), key;
for (key in dict) {
paper.getById(element.raphaelid).attr(key, ko.utils.unwrapObservable(dict[key]));
}
}
};
} |
NodesCreates a KO binding to handle node creation/removal | function createNodesBindingHandler(paper)
{ |
Store a list of node views | paper.nodes = []; |
Create the representation of a recently created node model Position is specified by the lastClick property of the paper, which is the last click event received by the paper (saved by us). | function createView(node)
{
var x, y, circle, label, st;
x = paper.lastClick.originalEvent.layerX;
y = paper.lastClick.originalEvent.layerY;
|
Create the Raphael objects | circle = paper.circle(x, y, 20).attr('fill', 'white');
label = paper.text(x, y, node.label());
st = paper.set().push(circle, label); |
Set the model on every object, so it's easy to get it in event handlers | circle.model = node;
label.model = node;
st.model = node;
node.circle = circle; |
Add the view to our list | paper.nodes.push(st); |
This is where we set up the KO bindings.
| $(circle.node).attr('data-bind', 'p1_r_attr: { stroke: color }');
$(label.node).attr('data-bind', 'p1_r_attr: { fill: color, text: label }');
ko.applyBindings(node, circle.node);
ko.applyBindings(node, label.node);
|
Fire node-clicked event and prevent the click from reaching the paper | st.click(function(event) {
eve('graph.node-clicked', node.graph, node);
event.preventDefault();
}); |
Currently the only case of dragging we care about is when a node is dropped on another node | st.drag(
null, // onmove
null, // onstart
function(event) {
var fromModel = node,
ontoView = paper.getElementByPoint(event.x, event.y); |
Is there an element where user stopped dragging? | if (ontoView != null) { |
If so, fire an appropriate event The event name will be graph.node-dnd-node or graph-node-dnd-edge The event handler will be called with the graph as context, arguments will be the nodes we're connecting | var ontoModel = ontoView.model;
eve('graph.node-dnd-' + ontoModel.type, fromModel.graph, fromModel, ontoModel);
}
}
);
}
return { |
This is the actual method that will be called by KO The relation between the number of nodes in the graph model and the views list declared above tells us what has happened: Requirements: - Only one element is added / removed at a time - Adding only happens at the end of the node array (push) - The graph property of a removed Node model is set to null before being removed | update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
var i,
models = ko.utils.unwrapObservable(valueAccessor()),
views = paper.nodes; |
The relation between the number of nodes in the graph model and the views list declared above tells us what has happened: | |
Nothing interesting | if (models.length == views.length) {
return; |
A new node has been created | } else if (models.length == views.length + 1) {
createView(models[models.length-1]); |
A node has been removed. Which one? We required that the graph property of removed nodes be set to null. This is where we use that, to find out which node to remove. | } else if (models.length == views.length - 1) {
for (i = 0; i < views.length; i++) {
if (views[i].model.graph == null) {
views[i].remove();
views.splice(i, 1);
break;
}
}
} else { |
This should never happen. | throw "Mayday!";
}
}
};
} |
EdgesCompletely analogous to It's probably possible to refactor some these two functions, they share a lot of code | function createEdgesBindingHandler(paper)
{
paper.edges = [];
function createView(edge)
{
var x, y, path, from, st;
x = paper.lastClick.originalEvent.layerX;
y = paper.lastClick.originalEvent.layerY;
window.to = edge.to;
path = paper.path('M' + edge.from.circle.attr('cx') + ',' + edge.from.circle.attr('cy') +
'L' + edge.to.circle.attr('cx') + ',' + edge.to.circle.attr('cy')
);
label = paper.text(
(edge.from.circle.attr('cx') + edge.to.circle.attr('cx')) / 2,
(edge.from.circle.attr('cy') + edge.to.circle.attr('cy')) / 2,
edge.label()
);
st = paper.set();
st.push(path, label);
label.model = edge;
path.model = edge;
st.model = edge;
edge.path = path;
paper.edges.push(st);
$(path.node).attr('data-bind', 'p1_r_attr: { stroke: color }');
$(label.node).attr('data-bind', 'p1_r_attr: { fill: color, text: label }');
st.click(function(event) {
eve('graph.edge-clicked', edge.graph, edge);
event.preventDefault();
});
ko.applyBindings(edge, path.node);
ko.applyBindings(edge, label.node);
}
return {
update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
var i,
models = ko.utils.unwrapObservable(valueAccessor()),
views = paper.edges;
if (models.length == views.length) {
return;
} else if (models.length == views.length + 1) {
createView(models[models.length-1]);
} else if (models.length == views.length - 1) {
for (i = 0; i < views.length; i++) {
if (views[i].model.graph == null) {
views[i].remove();
views.splice(i, 1);
break;
}
}
} else {
throw "Mayday!";
}
}
};
} |
Finally we set up the main interface | $(function() {
var g, paper, setAction;
g = new Graph();
paper = Raphael('canvas');
ko.bindingHandlers.nodes = createNodesBindingHandler(paper);
ko.bindingHandlers.edges = createEdgesBindingHandler(paper);
$('#canvas').data('graph', g).attr('data-bind', 'nodes: nodes, edges: edges');
ko.bindingHandlers.p1_r_attr = createRaphaelAttrBindingHandler(paper);
ko.applyBindings(g);
setAction = initActions(paper, g);
$('#controls input').each(function() {
$(this).click(function() { setAction(this.id); });
});
$('#canvas').click(function(event) {
paper.lastClick = event;
eve('graph.paper-clicked', g);
});
});
})();
|