var $node = function($parent, c, p) {
var $content = $('<p>').addClass('content');
var $ret = $('<div>').addClass('node').append($content).appendTo($parent);
$ret.data('connections', []);
$ret.content = function(c) {
if(c === undefined) {
return $(this).data('content');
}
else {
$(this).data('content', c);
$('.content', this).text(c);
}
};
content($ret, c);
xy($ret, p);
return $ret;
};
var content = function($e, c) {
if(c === undefined) {
return $e.data('content');
}
else {
$e.data('content', c);
$('.content', $e).text(c);
}
};
var xy = function($e, p) {
if(p === undefined) {
var pos = $e.position();
return {
x: pos.left + parseInt($e.css('width'), 10) / 2,
y: pos.top + parseInt($e.css('height'), 10) / 2
};
}
else {
$e.css('left', p.x - parseInt($e.css('width'), 10) / 2 + 'px');
$e.css('top', p.y - parseInt($e.css('height'), 10) / 2 + 'px');
}
};
var angle = function(x, y) {
var ret = Math.atan2(y, x);
ret += ret < 0? 2 * Math.PI: 0;
ret = Math.min(Math.PI * 2, ret);
return ret + Math.PI;
};
var dist = function(xOffset, yOffset) {
return Math.sqrt(xOffset * xOffset + yOffset * yOffset);
};
var $connection = function($parent, $p1, $p2) {
var $ret = $('<div>').addClass('line').appendTo($parent);
$ret.data('p1', $p1);
$ret.data('p2', $p2);
$ret.render = function() {
var $elem = $(this);
var $p1 = xy($elem.data('p1'));
var $p2 = xy($elem.data('p2'));
$elem.css('left', $p1.x + 'px');
$elem.css('top', $p1.y + 'px');
var xOffset = $p1.x - $p2.x;
var yOffset = $p1.y - $p2.y;
var rot = angle(xOffset, yOffset);
var d = dist(xOffset, yOffset);
$elem.css('-webkit-transform', 'rotate3d(0, 0, 1, ' + rot + 'rad)');
$elem.css('width', d + 'px');
};
return $ret;
};
var connect = function($parent, $a, $b) {
var $con = $connection($parent, $a, $b);
$a.data('connections').push($con);
$b.data('connections').push($con);
$con.render();
};
var render = function(e, ui) {
// e.shiftKey (bind to start) -> create new tmp node, if released on top of an existing one, create connection, else new node
$(this).data('connections').forEach(function(e) {
e.render();
});
};
var dragging = false;
var $parent = $('body');
var $a = $node($parent, 'foo', {x: 100, y: 100});
var $b = $node($parent, 'bar', {x: 400, y: 400});
var $c = $node($parent, 'baz', {x: 200, y: 300});
connect($parent, $a, $b);
connect($parent, $a, $c);
// apparently Draggable doesn't support live properly yet!
$('.node').live('mouseover', function() {
$(this).draggable().bind('drag', render).bind('dragstop', function(e, ui) {
render.apply(this, [e, ui]);
dragging = false;
}).bind('dragstart', function(e, ui) {
// XXX: this gets triggered for the newly created node too so we
// need to stash dragging state somewhere to avoid creating extra ones
if(!dragging && e.shiftKey) {
var $new = $(this);
var $old = $node($parent, content($new), xy($new));
connect($parent, $old, $new);
dragging = true;
}
});
});
$('.node').live('dblclick', function() {
var $input;
var $elem = $(this).children('.content');
if($elem[0].tagName == 'P') {
$input = $('<input>').attr('type', 'text').attr('value', $elem.text()).attr('size', 10).addClass('content').bind('keypress', function(e) {
if(e.keyCode == 13) {
var newText = $input.attr('value');
$input.replaceWith($elem);
content($elem.parent(), $input.attr('value'));
}
});
$elem.replaceWith($input);
}
});
<!DOCTYPE html>
<html>
<head>
<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1/themes/base/jquery-ui.css" rel="stylesheet" type="text/css" />
<script class="jsbin" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.js"></script>
<script class="jsbin" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/jquery-ui.min.js"></script>
<meta charset=utf-8 />
<title>JS Bin</title>
<!--[if IE]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<p>Drag nodes around, shift-drag to create new nodes, double click a node to edit its content (enter to confirm)</p>
</body>
</html>
article, aside, figure, footer, header, hgroup,
menu, nav, section { display: block; }
.node {
position: absolute;
-moz-border-radius: 100px;
-webkit-border-radius: 100px;
border-radius: 100px;
border: 3px solid black;
width: 100px;
height: 100px;
text-align: center;
background-color: white;
}
.node .content {
position: relative;
top: 40px;
}
.node input.content {
top: 40px;
}
.line {
position: absolute;
border: 2px solid black;
width: 400px;
z-index: -1;
-webkit-transform-origin: 0 0;
}