Converging client and webapp
authorveltr
Mon, 11 Feb 2013 18:08:19 +0100
changeset 56 a9b9e6c7be63
parent 55 17ff0be41a5b
child 57 01f66ed90e32
Converging client and webapp
client/js/main.js
client/js/models.js
client/js/paper-renderer.js
client/js/twitter-bin.js
client/js/wikipedia-bin.js
server/src/main/webapp/static/js/config.js
server/src/main/webapp/static/js/main.js
server/src/main/webapp/static/js/models.js
server/src/main/webapp/static/js/paper-renderer.js
--- a/client/js/main.js	Mon Feb 11 12:39:35 2013 +0100
+++ b/client/js/main.js	Mon Feb 11 18:08:19 2013 +0100
@@ -95,8 +95,12 @@
     if (typeof _opts.search !== "object" || !_opts.search) {
         _opts.search = [];
     }
+    if (typeof _opts.static_url !== "string") {
+        _opts.static_url = "";
+    }
     this.project = new Rkns.Models.Project();
     this.language = _opts.language;
+    this.static_url = _opts.static_url;
     this.l10n = Rkns.i18n[_opts.language];
     if (typeof _opts.user_id !== "undefined") {
         this.current_user = _opts.user_id;
@@ -109,6 +113,9 @@
     this.tabs = [];
     this.search_engines = [];
     this.selected_bin_item = undefined;
+
+    this.current_user_list = new Rkns.Models.UsersList();
+    
     var _this = this;
     this.$.mouseup(function() {
         _this.selected_bin_item = undefined;
--- a/client/js/models.js	Mon Feb 11 12:39:35 2013 +0100
+++ b/client/js/models.js	Mon Feb 11 18:08:19 2013 +0100
@@ -167,22 +167,22 @@
             },
           }
         ],
-        addUser: function(_props) {
+        addUser: function(_props, _options) {
             _props.project = this;
-            var _user = new User(_props);
-            this.get("users").push(_user);
+            var _user = User.findOrCreate(_props);
+            this.get("users").push(_user, _options);
             return _user;
         },
-        addNode: function(_props) {
-            _props.project = this;
-            var _node = new Node(_props);
-            this.get("nodes").push(_node);
+        addNode: function(_props, _options) {
+            _props.project = this;            
+            var _node = Node.findOrCreate(_props);
+            this.get("nodes").push(_node, _options);
             return _node;
         },
-        addEdge: function(_props) {
+        addEdge: function(_props, _options) {
             _props.project = this;
-            var _edge = new Edge(_props);
-            this.get("edges").push(_edge);
+            var _edge = Edge.findOrCreate(_props);
+            this.get("edges").push(_edge, _options);
             return _edge;
         },
         removeNode: function(_model) {
@@ -216,6 +216,55 @@
         }
     });
     
+    var RosterUser = Models.RosterUser = Backbone.Model.extend({
+        type: "roster_user",               
+        idAttribute : "_id",
+        
+        constructor: function(options) {
+            
+            if (typeof options !== "undefined") {
+                options._id = options._id || options.id || Models.getUID(this);
+                options.title = options.title || "(untitled " + this.type + ")";
+                options.description = options.description || "";
+                options.uri = options.uri || "";
+                options.project = options.project || null;
+                options.site_id = options.site_id || 0;
+                
+                if(typeof this.prepare === "function") {
+                    options = this.prepare(options);
+                }
+            }
+            Backbone.Model.prototype.constructor.call(this, options);
+        },
+        
+        validate: function() {
+            if(!this.type) {
+                return "object has no type";
+            }
+        },
+        
+        prepare: function(options) {
+            options.color = options.color || "#666666";
+            return options;
+        },
+        
+        toJSON: function() {
+            return {
+                _id: this.get("_id"),
+                title: this.get("title"),
+                uri: this.get("uri"),
+                description: this.get("description"),
+                color: this.get("color"),
+                project: (this.get("project") != null)?this.get("project").get("id"):null,
+                site_id: this.get("site_id")
+            };
+        },
+    });
+    
+    var UsersList = Models.UsersList = Backbone.Collection.extend({
+    	model: RosterUser
+    });
+    
 
 }).call(window);
 
--- a/client/js/paper-renderer.js	Mon Feb 11 12:39:35 2013 +0100
+++ b/client/js/paper-renderer.js	Mon Feb 11 18:08:19 2013 +0100
@@ -303,6 +303,13 @@
             this.node_image.position = this.paper_coords;
         }
     }
+        
+    Rkns._.each(this.project.get("edges").filter(function (ed) { return ((ed.to === this.model) || (ed.from === this.model));}), function(edge, index, list){
+        var repr = this.renderer.getRepresentationByModel(edge);
+    	if(repr != null && typeof repr.from_representation.paper_coords !== "undefined" && typeof repr.to_representation.paper_coords !== "undefined") {
+    		repr.redraw();
+    	}
+    }, this);
 }
 
 Rkns.Renderer.Node.prototype.paperShift = function(_delta) {
@@ -314,7 +321,7 @@
             }
         };
     this.model.set(_data);
-    this.renderer.redraw();
+    //this.renderer.redraw();
 }
 
 Rkns.Renderer.Node.prototype.openEditor = function() {
@@ -425,7 +432,7 @@
         _p1b = _p1a.add(_delta), /* to differentiate inbound and outbound links */
         _a = _v.angle,
         _handle = _v.divide(3),
-        _color = this.model.get("color") || this.model.get("color") || (this.model.get("created_by") || Rkns.Renderer._USER_PLACEHOLDER).get("color");;
+        _color = this.model.get("color") || this.model.get("color") || (this.model.get("created_by") || Rkns.Renderer._USER_PLACEHOLDER).get("color");
     this.paper_coords = _p0b.add(_p1b).divide(2);
     this.line.strokeColor = _color;
     this.line.segments[0].point = _p0a;
@@ -771,7 +778,7 @@
 Rkns.Renderer.NodeEditButton.prototype._init = function() {
     this.renderer.overlay_layer.activate();
     this.type = "Node-edit-button";
-    this.sector = Rkns.Renderer.Utils.sector(this, Rkns.Renderer._NODE_BUTTON_INNER, Rkns.Renderer._NODE_BUTTON_OUTER, - 90, 30, 1, 'img/edit.png', this.renderer.renkan.l10n.caption_edit);
+    this.sector = Rkns.Renderer.Utils.sector(this, Rkns.Renderer._NODE_BUTTON_INNER, Rkns.Renderer._NODE_BUTTON_OUTER, - 90, 30, 1, this.renderer.renkan.static_url+'img/edit.png', this.renderer.renkan.l10n.caption_edit);
 }
 
 Rkns.Renderer.NodeEditButton.prototype.moveTo = function(_pos) {
@@ -814,7 +821,7 @@
 Rkns.Renderer.NodeRemoveButton.prototype._init = function() {
     this.renderer.overlay_layer.activate();
     this.type = "Node-remove-button";
-    this.sector = Rkns.Renderer.Utils.sector(this, Rkns.Renderer._NODE_BUTTON_INNER, Rkns.Renderer._NODE_BUTTON_OUTER, -210, -90, 1, 'img/remove.png', this.renderer.renkan.l10n.caption_remove);
+    this.sector = Rkns.Renderer.Utils.sector(this, Rkns.Renderer._NODE_BUTTON_INNER, Rkns.Renderer._NODE_BUTTON_OUTER, -210, -90, 1, this.renderer.renkan.static_url+'img/remove.png', this.renderer.renkan.l10n.caption_remove);
 }
 
 Rkns.Renderer.NodeRemoveButton.prototype.moveTo = function(_pos) {
@@ -858,7 +865,7 @@
 Rkns.Renderer.NodeLinkButton.prototype._init = function() {
     this.renderer.overlay_layer.activate();
     this.type = "Node-link-button";
-    this.sector = Rkns.Renderer.Utils.sector(this, Rkns.Renderer._NODE_BUTTON_INNER, Rkns.Renderer._NODE_BUTTON_OUTER, 30, 150, 1, 'img/link.png', this.renderer.renkan.l10n.caption_link);
+    this.sector = Rkns.Renderer.Utils.sector(this, Rkns.Renderer._NODE_BUTTON_INNER, Rkns.Renderer._NODE_BUTTON_OUTER, 30, 150, 1, this.renderer.renkan.static_url+'img/link.png', this.renderer.renkan.l10n.caption_link);
 }
 
 Rkns.Renderer.NodeLinkButton.prototype.moveTo = function(_pos) {
@@ -895,7 +902,7 @@
 Rkns.Renderer.EdgeEditButton.prototype._init = function() {
     this.renderer.overlay_layer.activate();
     this.type = "Edge-edit-button";
-    this.sector = Rkns.Renderer.Utils.sector(this, Rkns.Renderer._EDGE_BUTTON_INNER, Rkns.Renderer._EDGE_BUTTON_OUTER, - 90, 90, 1, 'img/edit.png', this.renderer.renkan.l10n.caption_edit);
+    this.sector = Rkns.Renderer.Utils.sector(this, Rkns.Renderer._EDGE_BUTTON_INNER, Rkns.Renderer._EDGE_BUTTON_OUTER, - 90, 90, 1, this.renderer.renkan.static_url+'img/edit.png', this.renderer.renkan.l10n.caption_edit);
 }
 
 Rkns.Renderer.EdgeEditButton.prototype.moveTo = function(_pos) {
@@ -938,7 +945,7 @@
 Rkns.Renderer.EdgeRemoveButton.prototype._init = function() {
     this.renderer.overlay_layer.activate();
     this.type = "Edge-remove-button";
-    this.sector = Rkns.Renderer.Utils.sector(this, Rkns.Renderer._EDGE_BUTTON_INNER, Rkns.Renderer._EDGE_BUTTON_OUTER, - 270, -90, 1, 'img/remove.png', this.renderer.renkan.l10n.caption_remove);
+    this.sector = Rkns.Renderer.Utils.sector(this, Rkns.Renderer._EDGE_BUTTON_INNER, Rkns.Renderer._EDGE_BUTTON_OUTER, - 270, -90, 1, this.renderer.renkan.static_url+'img/remove.png', this.renderer.renkan.l10n.caption_remove);
 }
 Rkns.Renderer.EdgeRemoveButton.prototype.moveTo = function(_pos) {
     this.sector.moveTo(_pos);
--- a/client/js/twitter-bin.js	Mon Feb 11 12:39:35 2013 +0100
+++ b/client/js/twitter-bin.js	Mon Feb 11 18:08:19 2013 +0100
@@ -38,6 +38,7 @@
 );
 
 Rkns.Twitter.Bin.prototype._init = function(_renkan, _opts) {
+	this.renkan = _renkan;
     this.search = _opts.search;
     this.title_icon_$.addClass('Rk-Twitter-Title-Icon');
     this.title_$.html(this.search).addClass("Rk-Twitter-Title");
@@ -133,7 +134,7 @@
     } else {
         this.$.show();
     }
-    _renkan.resizeBins();
+    this.renkan.resizeBins();
 }
 
 Rkns.Twitter.Bin.prototype.refresh = function() {
--- a/client/js/wikipedia-bin.js	Mon Feb 11 12:39:35 2013 +0100
+++ b/client/js/wikipedia-bin.js	Mon Feb 11 18:08:19 2013 +0100
@@ -27,12 +27,13 @@
 
 Rkns.Wikipedia.Bin.prototype.resultTemplate = Rkns._.template(
     '<li class="Rk-Wikipedia-Result Rk-Bin-Item" data-uri="<%-url%>" '
-    + 'data-title="Wikipedia: <%-title%>" data-description="<%-description%>" data-image="img/wikipedia.png">'
-    + '<img class="Rk-Wikipedia-Icon" src="img/wikipedia.png"></div><h4 class="Rk-Wikipedia-Title"><a href="<%-url%>" target="_blank"><%=htitle%></a></h4>'
+    + 'data-title="Wikipedia: <%-title%>" data-description="<%-description%>" data-image="<%-static_url%>img/wikipedia.png">'
+    + '<img class="Rk-Wikipedia-Icon" src="<%-static_url%>img/wikipedia.png"></div><h4 class="Rk-Wikipedia-Title"><a href="<%-url%>" target="_blank"><%=htitle%></a></h4>'
     + '<p class="Rk-Wikipedia-Snippet"><%=hdescription%></p></li>'
 );
 
 Rkns.Wikipedia.Bin.prototype._init = function(_renkan, _opts) {
+	this.renkan = _renkan;
     this.search = _opts.search;
     this.lang = _opts.lang || "en";
     this.title_icon_$.addClass('Rk-Wikipedia-Title-Icon Rk-Wikipedia-Lang-' + this.lang);
@@ -64,7 +65,8 @@
             title: title,
             htitle: highlight(title),
             description: description,
-            hdescription: highlight(description)
+            hdescription: highlight(description),
+            static_url: _this.renkan.static_url
         });
     });
     _this.main_$.html(_html);
@@ -78,7 +80,7 @@
     } else {
         this.$.show();
     }
-    _renkan.resizeBins();
+    this.renkan.resizeBins();
 }
     
 Rkns.Wikipedia.Bin.prototype.refresh = function() {
--- a/server/src/main/webapp/static/js/config.js	Mon Feb 11 12:39:35 2013 +0100
+++ b/server/src/main/webapp/static/js/config.js	Mon Feb 11 18:08:19 2013 +0100
@@ -34,7 +34,7 @@
 
 var cowebConfig = {
     baseUrl: window.location.protocol + '//' +window.location.host,
-	adminUrl : '/renkan/admin',
+	adminUrl : '/admin',
 	debug: false,
 	//useWebSockets: true
 };
--- a/server/src/main/webapp/static/js/main.js	Mon Feb 11 12:39:35 2013 +0100
+++ b/server/src/main/webapp/static/js/main.js	Mon Feb 11 18:08:19 2013 +0100
@@ -26,6 +26,8 @@
 
 Rkns._ = _;
 
+Rkns.pickerColors = ["#8f1919", "#a80000", "#d82626", "#ff0000", "#e87c7c", "#ff6565", "#f7d3d3", "#fecccc", "#8f5419", "#a85400", "#d87f26", "#ff7f00", "#e8b27c", "#ffb265", "#f7e5d3", "#fee5cc", "#8f8f19", "#a8a800", "#d8d826", "#feff00", "#e8e87c", "#feff65", "#f7f7d3", "#fefecc", "#198f19", "#00a800", "#26d826", "#00ff00", "#7ce87c", "#65ff65", "#d3f7d3", "#ccfecc", "#198f8f", "#00a8a8", "#26d8d8", "#00feff", "#7ce8e8", "#65feff", "#d3f7f7", "#ccfefe", "#19198f", "#0000a8", "#2626d8", "#0000ff", "#7c7ce8", "#6565ff", "#d3d3f7", "#ccccfe", "#8f198f", "#a800a8", "#d826d8", "#ff00fe", "#e87ce8", "#ff65fe", "#f7d3f7", "#feccfe", "#000000", "#242424", "#484848", "#6d6d6d", "#919191", "#b6b6b6", "#dadada", "#ffffff"];
+
 Rkns._BaseBin = function(_renkan, _opts) {
     if (typeof _renkan !== "undefined") {
         this.renkan = _renkan;
@@ -120,12 +122,12 @@
         _this.$.find(".Rk-Bin-Item.dragging").removeClass("dragging");
     });
     if (!_opts.search.length) {
-        this.$.find(".Rk-Search-Form").detach();
+        this.$.find(".Rk-Web-Search-Form").detach();
     } else {
         var _tmpl = Rkns._.template('<li class="<%= className %>" data-key="<%= key %>"><%= title %></li>'),
             _select = this.$.find(".Rk-Search-List"),
-            _input = this.$.find(".Rk-Search-Input"),
-            _form = this.$.find(".Rk-Search-Form");
+            _input = this.$.find(".Rk-Web-Search-Input")
+            _form = this.$.find(".Rk-Web-Search-Form");
         Rkns._(_opts.search).each(function(_search, _key) {
             var _searchObj = new _search.type(_this, _search);
             _this.search_engines.push(_searchObj);
@@ -196,13 +198,25 @@
     Rkns.$(window).resize(function() {
         _this.resizeBins();
     });
+    
+    this.$.find(".Rk-Bins-Search-Input").on("change keyup paste input", function() {
+       var val = Rkns.$(this).val();
+       Rkns._(_this.tabs).each(function(tab) {
+           tab.render(val);
+       });
+    });
+    this.$.find(".Rk-Bins-Search-Form").submit(function() {
+        return false
+    });
 }
 
 Rkns.Renkan.prototype.template = Rkns._.template(
     '<div class="Rk-Title"><h1>Hyper Plateau</h1></div><div class="Rk-Bins">'
-    + '<form class="Rk-Search-Form"><input class="Rk-Search-Input" type="text" placeholder="Search" />'
+    + '<form class="Rk-Web-Search-Form Rk-Search-Form"><input class="Rk-Web-Search-Input Rk-Search-Input" type="search" placeholder="Search the Web" />'
     + '<div class="Rk-Search-Select"><div class="Rk-Search-Current"></div><ul class="Rk-Search-List"></ul></div>'
-    + '<input type="submit" value="" class="Rk-Search-Submit" /></form>'
+    + '<input type="submit" value="" class="Rk-Web-Search-Submit Rk-Search-Submit" /></form>'
+    + '<form class="Rk-Bins-Search-Form Rk-Search-Form"><input class="Rk-Bins-Search-Input Rk-Search-Input" type="search" placeholder="Search in Bins" />'
+    + '<input type="submit" value="" class="Rk-Bins-Search-Submit Rk-Search-Submit" /></form>'
     + '<ul class="Rk-Bin-List"></ul></div><div class="Rk-Render Rk-Render-Panel"></div>'
 );
 
@@ -213,8 +227,8 @@
 }
 
 Rkns.Renkan.prototype.resizeBins = function() {
-    var _d = + this.$.find(".Rk-Search-Form").outerHeight();
-    this.$.find(".Rk-Bin-Title").each(function() {
+    var _d = + this.$.find(".Rk-Web-Search-Form").outerHeight() + this.$.find(".Rk-Bins-Search-Form").outerHeight();
+    this.$.find(".Rk-Bin-Title:visible").each(function() {
         _d += Rkns.$(this).outerHeight();
     });
     this.$.find(".Rk-Bin-Main").css({
--- a/server/src/main/webapp/static/js/models.js	Mon Feb 11 12:39:35 2013 +0100
+++ b/server/src/main/webapp/static/js/models.js	Mon Feb 11 18:08:19 2013 +0100
@@ -88,6 +88,7 @@
                 description: this.get("description"),
                 position: this.get("position"),
                 image: this.get("image"),
+                color: this.get("color"),
                 created_by: this.get("created_by") ? this.get("created_by").get("_id") : null
             };
         },
@@ -128,6 +129,7 @@
                 description: this.get("description"),
                 from: this.get("from") ? this.get("from").get("_id") : null,
                 to: this.get("to") ? this.get("to").get("_id") : null,
+                color: this.get("color"),
                 created_by: this.get("created_by") ? this.get("created_by").get("_id") : null
             };
         },
--- a/server/src/main/webapp/static/js/paper-renderer.js	Mon Feb 11 12:39:35 2013 +0100
+++ b/server/src/main/webapp/static/js/paper-renderer.js	Mon Feb 11 18:08:19 2013 +0100
@@ -20,7 +20,14 @@
     _EDITOR_GRADIENT : new paper.Gradient(['#f0f0f0', '#d0d0d0']),
     _CLICKMODE_ADDNODE : 1,
     _CLICKMODE_STARTEDGE : 2,
-    _CLICKMODE_ENDEDGE : 3
+    _CLICKMODE_ENDEDGE : 3,
+    _USER_PLACEHOLDER : {
+        color: "#000000",
+        title: "(unknown user)",
+        get: function(attr) {
+            return this[attr] || false;
+        }
+    }
 }
 
 Rkns.Renderer.Utils = {
@@ -236,6 +243,7 @@
     this.circle = new paper.Path.Circle([0, 0], Rkns.Renderer._NODE_RADIUS);
     this.circle.fillColor = '#ffffff';
     this.circle.__representation = this;
+    this.circle.strokeWidth = 2;
     this.title = new paper.PointText([0,0]);
     this.title.characterStyle = {
         fontSize: Rkns.Renderer._NODE_FONT_SIZE,
@@ -256,7 +264,7 @@
     this.circle.position = this.paper_coords;
     this.title.content = this.model.get("title");
     this.title.position = this.paper_coords.add([0, Rkns.Renderer._NODE_RADIUS + 1.5 *Rkns.Renderer._NODE_FONT_SIZE]);
-    this.circle.strokeColor = this.model.get("created_by").get("color");
+    this.circle.strokeColor = this.model.get("color") || (this.model.get("created_by") || Rkns.Renderer._USER_PLACEHOLDER).get("color");
     this.edit_button.moveTo(this.paper_coords);
     this.remove_button.moveTo(this.paper_coords);
     this.link_button.moveTo(this.paper_coords);
@@ -324,7 +332,7 @@
 }
 
 Rkns.Renderer.Node.prototype.select = function() {
-    this.circle.strokeWidth = 3;
+    this.circle.strokeWidth = 4;
     this.edit_button.show();
     this.remove_button.show();
     this.link_button.show();
@@ -342,7 +350,7 @@
         this.edit_button.hide();
         this.remove_button.hide();
         this.link_button.hide();
-        this.circle.strokeWidth = 1;
+        this.circle.strokeWidth = 2;
         Rkns.$('.Rk-Bin-Item').removeClass("selected");
     }
 }
@@ -394,6 +402,7 @@
     this.line = new paper.Path();
     this.line.add([0,0],[0,0],[0,0]);
     this.line.__representation = this;
+    this.line.strokeWidth = 2;
     this.arrow = new paper.Path();
     this.arrow.add([0,0],[Rkns.Renderer._ARROW_LENGTH,Rkns.Renderer._ARROW_WIDTH / 2],[0,Rkns.Renderer._ARROW_WIDTH]);
     this.arrow.__representation = this;
@@ -423,7 +432,7 @@
         _p1b = _p1a.add(_delta), /* to differentiate inbound and outbound links */
         _a = _v.angle,
         _handle = _v.divide(3),
-        _color = this.model.get("created_by").get("color");
+        _color = this.model.get("color") || this.model.get("color") || (this.model.get("created_by") || Rkns.Renderer._USER_PLACEHOLDER).get("color");
     this.paper_coords = _p0b.add(_p1b).divide(2);
     this.line.strokeColor = _color;
     this.line.segments[0].point = _p0a;
@@ -457,7 +466,7 @@
 }
 
 Rkns.Renderer.Edge.prototype.select = function() {
-    this.line.strokeWidth = 3;
+    this.line.strokeWidth = 4;
     this.edit_button.show();
     this.remove_button.show();
 }
@@ -466,7 +475,7 @@
     if (!_newTarget || _newTarget.edge_representation !== this) {
         this.edit_button.hide();
         this.remove_button.hide();
-        this.line.strokeWidth = 1;
+        this.line.strokeWidth = 2;
     }
 }
 
@@ -505,7 +514,7 @@
     this.renderer.edge_layer.activate();
     this.type = "temp-edge";
     
-    var _color = this.project.get("users").get(this.renderer.renkan.current_user).get("color");
+    var _color = this.project.get("users").get(this.renderer.renkan.current_user).get("color") || "#000000";
     this.line = new paper.Path();
     this.line.strokeColor = _color;
     this.line.add([0,0],[0,0]);
@@ -597,12 +606,15 @@
     + '<p><label><%=l10n.edit_title%></label><input class="Rk-Edit-Title" type="text" value="<%=node.title%>"/></p>'
     + '<p><label><%=l10n.edit_uri%></label><input class="Rk-Edit-URI" type="text" value="<%=node.uri%>"/><a class="Rk-Edit-Goto" href="<%=node.uri%>" target="_blank"></a></p>'
     + '<p><label><%=l10n.edit_description%></label><textarea class="Rk-Edit-Description"><%=node.description%></textarea></p>'
+    + '<div class="Rk-Editor-p"><label>Node color</label><div class="Rk-Edit-ColorPicker-Wrapper"><span class="Rk-Edit-Color" style="background:<%=node.color%>;"><span class="Rk-Edit-ColorTip"></span></span><ul class="Rk-Edit-ColorPicker">'
+    + '<% print(Rkns.pickerColors.reduce(function(m,c) { return m + "<li data-color=\'" + c + "\' style=\'background: " + c + "\'></li>"},"")); %></ul><span class="Rk-Edit-ColorPicker-Text">Choose color</span></div></div>'
     + '<p><label><%=l10n.edit_image%></label><input class="Rk-Edit-Image" type="text" value="<%=node.image%>"/><img class="Rk-Edit-ImgPreview" src="<%=node.image%>" /></p>'
     + '<p><label><%=l10n.created_by%></label> <span class="Rk-UserColor" style="background:<%=node.created_by_color%>;"></span><%=node.created_by_title%></p>'
 );
 
 Rkns.Renderer.NodeEditor.prototype.draw = function() {
-    var _model = this.node_representation.model;
+    var _model = this.node_representation.model,
+        _created_by = _model.get("created_by") || Rkns.Renderer._USER_PLACEHOLDER;
     this.editor_$
         .html(this.template({
             node: {
@@ -610,8 +622,9 @@
                 uri: _model.get("uri"),
                 description: _model.get("description"),
                 image: _model.get("image") || "",
-                created_by_color: _model.get("created_by").get("color"),
-                created_by_title: _model.get("created_by").get("title")
+                color: _model.get("color") || _created_by.get("color"),
+                created_by_color: _created_by.get("color"),
+                created_by_title: _created_by.get("title")
             },
             l10n: this.renderer.renkan.l10n
         }));
@@ -639,6 +652,17 @@
         _this.redraw();
     })
     this.editor_$.find(".Rk-Edit-Title")[0].focus();
+    this.editor_$.find(".Rk-Edit-ColorPicker-Wrapper").hover(
+        function() { _this.editor_$.find(".Rk-Edit-ColorPicker").show(); },
+        function() { _this.editor_$.find(".Rk-Edit-ColorPicker").hide(); }
+    );
+    this.editor_$.find(".Rk-Edit-ColorPicker li").hover(
+        function() { _this.editor_$.find(".Rk-Edit-Color").css("background", $(this).attr("data-color")); },
+        function() { _this.editor_$.find(".Rk-Edit-Color").css("background", _model.get("color") || (_model.get("created_by") || Rkns.Renderer._USER_PLACEHOLDER).get("color")) }
+    ).click(function() {
+        _model.set("color", $(this).attr("data-color"));
+        _this.redraw();
+    });
 }
 
 Rkns.Renderer.NodeEditor.prototype.redraw = function() {
@@ -680,25 +704,31 @@
     '<h2><span class="Rk-CloseX">&times;</span><%=l10n.edit_edge%></span></h2>'
     + '<p><label><%=l10n.edit_title%></label><input class="Rk-Edit-Title" type="text" value="<%=edge.title%>"/></p>'
     + '<p><label><%=l10n.edit_uri%></label><input class="Rk-Edit-URI" type="text" value="<%=edge.uri%>"/><a class="Rk-Edit-Goto" href="<%=edge.uri%>" target="_blank"></a></p>'
-    + '<p><label><%=l10n.edit_from%></label><span class="Rk-UserColor" style="background:<%=edge.from_created_by_color%>;"></span><%=edge.from_title%></p>'
-    + '<p><label><%=l10n.edit_to%></label><span class="Rk-UserColor" style="background:<%=edge.to_created_by_color%>;"></span><%=edge.to_title%></p>'
+    + '<div class="Rk-Editor-p"><label>Edge color:</label><div class="Rk-Edit-ColorPicker-Wrapper"><span class="Rk-Edit-Color" style="background:<%=edge.color%>;"><span class="Rk-Edit-ColorTip"></span></span><ul class="Rk-Edit-ColorPicker">'
+    + '<% print(Rkns.pickerColors.reduce(function(m,c) { return m + "<li data-color=\'" + c + "\' style=\'background: " + c + "\'></li>"},"")); %></ul><span class="Rk-Edit-ColorPicker-Text">Choose color</span></div></div>'
+    + '<p><label><%=l10n.edit_from%></label><span class="Rk-UserColor" style="background:<%=edge.from_color%>;"></span><%=edge.from_title%></p>'
+    + '<p><label><%=l10n.edit_to%></label><span class="Rk-UserColor" style="background:<%=edge.to_color%>;"></span><%=edge.to_title%></p>'
     + '<p><label><%=l10n.created_by%> </label><span class="Rk-UserColor" style="background:<%=edge.created_by_color%>;"></span><%=edge.created_by_title%></p>'
 );
 
 Rkns.Renderer.EdgeEditor.prototype.draw = function() {
-    var _model = this.edge_representation.model;
+    var _model = this.edge_representation.model,
+        _from_model = _model.get("from"),
+        _to_model = _model.get("to"),
+        _created_by = _model.get("created_by") || Rkns.Renderer._USER_PLACEHOLDER;
     this.editor_$
         .html(this.template({
             edge: {
                 title: _model.get("title"),
                 uri: _model.get("uri"),
                 description: _model.get("description"),
-                from_title: _model.get("from").get("title"),
-                to_title: _model.get("to").get("title"),
-                from_created_by_color: _model.get("from").get("created_by").get("color"),
-                to_created_by_color: _model.get("to").get("created_by").get("color"),
-                created_by_color: _model.get("created_by").get("color"),
-                created_by_title: _model.get("created_by").get("title")
+                color: _model.get("color") || _created_by.get("color"),
+                from_title: _from_model.get("title"),
+                to_title: _to_model.get("title"),
+                from_color: _from_model.get("color") || (_from_model.get("created_by") || Rkns.Renderer._USER_PLACEHOLDER).get("color"),
+                to_color: _to_model.get("color") || (_to_model.get("created_by") || Rkns.Renderer._USER_PLACEHOLDER).get("color"),
+                created_by_color: _created_by.get("color"),
+                created_by_title: _created_by.get("title")
             },
             l10n: this.renderer.renkan.l10n
         }));
@@ -717,6 +747,17 @@
         _model.set(_data);
         _this.redraw();
     });
+    this.editor_$.find(".Rk-Edit-ColorPicker-Wrapper").hover(
+        function() { _this.editor_$.find(".Rk-Edit-ColorPicker").show(); },
+        function() { _this.editor_$.find(".Rk-Edit-ColorPicker").hide(); }
+    );
+    this.editor_$.find(".Rk-Edit-ColorPicker li").hover(
+        function() { _this.editor_$.find(".Rk-Edit-Color").css("background", $(this).attr("data-color")); },
+        function() { _this.editor_$.find(".Rk-Edit-Color").css("background", _model.get("color") || (_model.get("created_by") || Rkns.Renderer._USER_PLACEHOLDER).get("color")); }
+    ).click(function() {
+        _model.set("color", $(this).attr("data-color"));
+        _this.redraw();
+    });
 }
 Rkns.Renderer.EdgeEditor.prototype.redraw = function() {
     var _coords = this.edge_representation.paper_coords;