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; }