├── Procfile ├── .gitignore ├── .env.example ├── package.json ├── bower.json ├── force_view.css ├── my-backbone-model.js ├── main.js ├── dbaas.js ├── index.html ├── server.js ├── my-backbone-view.js └── force-view.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | .DS_Store 4 | *.log 5 | .env 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | #COMPOSE_URI=mongodb://example:example@127.0.0.1:27017/graph 2 | COMPOSE_URI=mongodb://localhost:27017/graph 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pubsub-with-backbone-events", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "body-parser": "^1.13.1", 6 | "bower": "^1.4.1", 7 | "dotenv": "^5.0.1", 8 | "express": "^3.4.7", 9 | "extend": "^3.0.0", 10 | "mongoose": "^4.1.2", 11 | "socket.io": "^1.3.6" 12 | }, 13 | "engines": { 14 | "node": ">=0.10.25" 15 | }, 16 | "scripts": { 17 | "postinstall": "node ./node_modules/bower/bin/bower install" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pubsub-with-backbone-events", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/igorlima/pubsub-with-backbone-events", 5 | "authors": [ 6 | "Igor Ribeiro Lima " 7 | ], 8 | "dependencies": { 9 | "backbone": "~1.2.1", 10 | "bootstrap": "~3.3.5", 11 | "d3": "~2.10.3", 12 | "jquery": "~2.1.4", 13 | "requirejs-bower": "~2.1.19", 14 | "underscore": "~1.8.3", 15 | "bootstrap-colorpicker": "~2.2.0" 16 | }, 17 | "license": "MIT", 18 | "private": true, 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "tests" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /force_view.css: -------------------------------------------------------------------------------- 1 | /* body { 2 | font: 13px sans-serif; 3 | position: relative; 4 | width: 960px; 5 | height: 500px; 6 | } */ 7 | 8 | .node { 9 | fill: #000; 10 | cursor: crosshair; 11 | } 12 | 13 | .node_selected { 14 | fill: #ff7f0e; 15 | stroke: #ff7f0e; 16 | } 17 | 18 | .drag_line { 19 | stroke: #999; 20 | stroke-width: 5; 21 | pointer-events: none; 22 | } 23 | 24 | .drag_line_hidden { 25 | stroke: #999; 26 | stroke-width: 0; 27 | pointer-events: none; 28 | } 29 | 30 | .link { 31 | stroke: #999; 32 | stroke-width: 5; 33 | cursor: crosshair; 34 | } 35 | 36 | .link_selected { 37 | stroke: #ff7f0e; 38 | } 39 | 40 | .twitter-typeahead { 41 | width: 100%; 42 | } 43 | -------------------------------------------------------------------------------- /my-backbone-model.js: -------------------------------------------------------------------------------- 1 | define(['backbone'], function(Backbone) { 2 | 3 | return Backbone.Model.extend({ 4 | 5 | initialize: function( options ) { 6 | this.on('change:id', this.notifyEachIdChange, this); 7 | this.on('change:color', this.notifyEachColorChange, this); 8 | this.on('change:label', this.notifyEachLabelChange, this); 9 | }, 10 | 11 | notifyEachIdChange: function() { 12 | console.log( 'id was changed' ); 13 | }, 14 | 15 | notifyEachColorChange: function() { 16 | console.log( 'color was changed' ); 17 | }, 18 | 19 | notifyEachLabelChange: function() { 20 | console.log( 'label was changed' ); 21 | } 22 | 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | baseUrl: '/', 3 | paths: { 4 | backbone: 'bower_components/backbone/backbone-min', 5 | bootstrap: 'bower_components/bootstrap/dist/js/bootstrap.min', 6 | colorpicker: 'bower_components/bootstrap-colorpicker/dist/js/bootstrap-colorpicker.min', 7 | d3: 'bower_components/d3/d3.v2.min', 8 | jquery: 'bower_components/jquery/dist/jquery.min', 9 | underscore: 'bower_components/underscore/underscore-min', 10 | 11 | forceView: 'force-view', 12 | myView: 'my-backbone-view', 13 | myModel: 'my-backbone-model', 14 | dbaas: 'dbaas', 15 | io: '/socket.io/socket.io' 16 | }, 17 | shim: { 18 | bootstrap: { 19 | deps: ['jquery'] 20 | }, 21 | d3: { 22 | exports: 'd3' 23 | }, 24 | colorpicker: ['jquery'] 25 | } 26 | }); 27 | 28 | require( [ 'myView', 'bootstrap', 'colorpicker' ], function( MyView ) { 29 | new MyView(); 30 | } ); 31 | -------------------------------------------------------------------------------- /dbaas.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 'backbone', 'io'], function($, Backbone, io) { 2 | var Channel; 3 | 4 | var socket = io(); 5 | 6 | Channel = $.extend( {}, Backbone.Events ); 7 | 8 | Channel.on('disconnect', function() { 9 | console.warn('disconnected'); 10 | }); 11 | 12 | Channel.on('retrieve-all-nodes', function() { 13 | socket.emit('retrieve-all-nodes'); 14 | socket.on( 'node-added', function(node) { 15 | Channel.trigger( 'node-added', node ); 16 | } ); 17 | 18 | socket.on( 'node-edited', function(node) { 19 | Channel.trigger( 'node-edited', node ); 20 | } ); 21 | 22 | socket.on( 'node-removed', function(node) { 23 | Channel.trigger( 'node-removed', node ); 24 | } ); 25 | 26 | socket.on( 'link-added', function(link) { 27 | Channel.trigger( 'link-added', link ); 28 | } ); 29 | 30 | socket.on( 'link-removed', function(link) { 31 | Channel.trigger( 'link-removed', link ); 32 | } ); 33 | 34 | }); 35 | 36 | Channel.on('add-node', function( node, cb ) { 37 | socket.emit( 'add-node', node, function(obj) { 38 | if (cb) { 39 | node.id = obj.id; 40 | cb(node); 41 | } 42 | } ); 43 | }); 44 | 45 | Channel.on('edit-node', function(node) { 46 | if (node && node.id) { 47 | socket.emit( 'edit-node', node ); 48 | } 49 | } ); 50 | 51 | Channel.on('remove-node', function(node) { 52 | if (node && node.id) { 53 | socket.emit( 'remove-node', node ); 54 | } 55 | }); 56 | 57 | Channel.on('add-link', function(link) { 58 | if (!link || !link.source || !link.target || !link.source.id || !link.target.id) return; 59 | 60 | socket.emit( 'add-link', link, function(obj) { 61 | link.id = obj.id; 62 | Channel.trigger( 'link-added', link ); 63 | } ); 64 | 65 | }); 66 | 67 | Channel.on('remove-link', function(link) { 68 | if (link && link.id) { 69 | socket.emit( 'remove-link', link, function() { 70 | Channel.trigger( 'link-removed', link ); 71 | } ); 72 | } 73 | }); 74 | 75 | return Channel; 76 | }); 77 | 78 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Sample App 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | 41 |
42 |
43 | 44 | 45 | 71 | 72 | 73 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | 4 | var express = require('express'), 5 | mongoose = require('mongoose'), 6 | bodyParser = require('body-parser'), 7 | app = express(), 8 | http = require('http').Server(app), 9 | io = require('socket.io')(http), 10 | extend = require('extend'), 11 | 12 | // Mongoose Schema definition 13 | Edge = mongoose.model('Edge', { 14 | id: String, 15 | source: { 16 | id: String, 17 | weight: Number 18 | }, 19 | target: { 20 | id: String, 21 | weight: Number 22 | } 23 | }), 24 | 25 | Vertex = mongoose.model('Vertex', { 26 | id: String, 27 | color: String, 28 | label: String 29 | }); 30 | 31 | /* 32 | * I’m sharing my credential here. 33 | * Feel free to use it while you’re learning. 34 | * After that, create and use your own credential. 35 | * Thanks. 36 | * 37 | * COMPOSE_URI=mongodb://example:example@dogen.mongohq.com:10089/graph 38 | * COMPOSE_URI=mongodb://example:example@127.0.0.1:27017/graph 39 | * 'mongodb://example:example@dogen.mongohq.com:10089/graph' 40 | */ 41 | mongoose.connect(process.env.COMPOSE_URI, function (error) { 42 | if (error) console.error(error); 43 | else console.log('mongo connected'); 44 | }); 45 | /** END */ 46 | 47 | 48 | app 49 | .use(express.static(__dirname + '/')) 50 | // https://scotch.io/tutorials/use-expressjs-to-get-url-and-post-parameters 51 | .use(bodyParser.json()) // support json encoded bodies 52 | .use(bodyParser.urlencoded({ extended: true })) // support encoded bodies 53 | ; 54 | 55 | http.listen(process.env.PORT || 5000, function(){ 56 | console.log('listening on *:5000'); 57 | }); 58 | 59 | io.on('connection', function(socket) { 60 | 61 | function removeLinkIfNodeWasDeleted(linkId, nodeId) { 62 | Vertex.findById( nodeId, function(err, vertex) { 63 | if (vertex) { 64 | return; 65 | } 66 | Edge.findById(linkId, function(err, link) { 67 | link.remove(); 68 | }); 69 | } ); 70 | } 71 | 72 | function cleanUpLinkIfNeeded(link) { 73 | removeLinkIfNodeWasDeleted( link.id, link.source.id ); 74 | removeLinkIfNodeWasDeleted( link.id, link.target.id ); 75 | } 76 | 77 | console.log('a user connected'); 78 | socket.on('disconnect', function(){ 79 | console.log('user disconnected'); 80 | }); 81 | 82 | socket.on('retrieve-all-nodes', function() { 83 | Vertex.find( function( err, nodes) { 84 | nodes.forEach(function(node) { 85 | node.id = node._id; 86 | socket.emit( 'node-added', node ); 87 | }); 88 | Edge.find( function(err, links) { 89 | links.forEach(function(link) { 90 | link.id = link._id; 91 | socket.emit( 'link-added', link ); 92 | cleanUpLinkIfNeeded( link ); 93 | }); 94 | } ); 95 | }); 96 | }); 97 | 98 | // socket.on('remove-all-nodes', function() { 99 | // ... 100 | // }); 101 | 102 | socket.on('add-node', function( node, cb ) { 103 | var vertex = new Vertex( node ); 104 | node.id = vertex._id; 105 | vertex.save(function (err) { 106 | cb && cb(node); 107 | socket.broadcast.emit( 'node-added', node ); 108 | socket.emit( 'node-added', node ); 109 | }); 110 | }); 111 | 112 | socket.on('edit-node', function(node) { 113 | if (node && node.id) { 114 | Vertex.findById( node.id, function (err, vertex) { 115 | vertex.label = node.label; 116 | vertex.color = node.color; 117 | vertex.save( function(err) { 118 | socket.emit( 'node-edited', node ); 119 | socket.broadcast.emit( 'node-edited', node ); 120 | }); 121 | } ); 122 | } 123 | } ); 124 | 125 | socket.on('remove-node', function(node) { 126 | if (node && node.id) { 127 | Vertex.findById( node.id, function(err, vertex) { 128 | vertex.remove( function(err) { 129 | socket.emit( 'node-removed', node ); 130 | socket.broadcast.emit( 'node-removed', node ); 131 | }); 132 | } ); 133 | } 134 | }); 135 | 136 | socket.on('add-link', function(link, cb) { 137 | var edge = new Edge( link ); 138 | link.id = edge._id; 139 | edge.save( function(err) { 140 | cb && cb(link); 141 | socket.broadcast.emit( 'link-added', link ); 142 | socket.emit( 'link-added', link ); 143 | } ); 144 | }); 145 | 146 | socket.on('remove-link', function(link) { 147 | if (link && link.id) { 148 | Edge.findById( link.id, function(err, edge) { 149 | edge.remove( function(err) { 150 | socket.broadcast.emit( 'link-removed', link ); 151 | socket.emit( 'link-removed', link ); 152 | } ); 153 | }); 154 | } 155 | }); 156 | 157 | }); 158 | 159 | -------------------------------------------------------------------------------- /my-backbone-view.js: -------------------------------------------------------------------------------- 1 | 2 | define(['jquery', 'backbone', 'myModel', 'forceView'], function($, Backbone, MyModel, ForceView) { 3 | 4 | return Backbone.View.extend({ 5 | el: 'body', 6 | events: { 7 | 'click button.add-node': 'addNode', 8 | 'click #editNodeModal button.btn.btn-primary': 'editNode', 9 | 'change #textNode': 'textChanged', 10 | 'changeColor #textColorNode': 'colorChanged' 11 | }, 12 | 13 | initialize: function( options ) { 14 | var view = this; 15 | view.mediatorChannel = $.extend( {}, Backbone.Events ); 16 | view.model = new MyModel(); 17 | view.$el.find('#textColorNode').colorpicker(); 18 | 19 | ForceView.trigger('init', function() { 20 | view.sync(); 21 | }); 22 | }, 23 | 24 | textChanged: function() { 25 | this.model.set( 'label', this.$el.find('#textNode').val(), { silent: true } ); 26 | }, 27 | 28 | colorChanged: function() { 29 | this.model.set( 'color', this.$el.find('#textColorNode').val(), { silent: true } ); 30 | }, 31 | 32 | openModal: function() { 33 | this.$el.find('#editNodeModal').modal('show'); 34 | }, 35 | 36 | hideModal: function() { 37 | this.$el.find('#editNodeModal').modal('hide'); 38 | }, 39 | 40 | editNode: function() { 41 | this.mediatorChannel.trigger('edit-node', { 42 | id: this.model.get('id'), 43 | color: this.model.get('color'), 44 | label: this.model.get('label') 45 | } ); 46 | this.hideModal(); 47 | }, 48 | 49 | addNode: function() { 50 | this.mediatorChannel.trigger('add-node'); 51 | }, 52 | 53 | sync: function() { 54 | var view = this; 55 | 56 | view.model.on('change:color', function(model, color) { 57 | view.$el.find('#textColorNode').val( color ); 58 | }); 59 | view.model.on('change:label', function(model, label) { 60 | view.$el.find('#textNode').val( label ); 61 | }); 62 | 63 | view.syncWithForceView(); 64 | view.syncWithDBaaS(); 65 | console.log('ready!! backbone view loaded and sync'); 66 | }, 67 | 68 | syncWithDBaaS: function() { 69 | var mediatorChannel = this.mediatorChannel; 70 | require(['dbaas'], function(dbaas) { 71 | 72 | mediatorChannel.on('remove-node', function(node) { 73 | dbaas.trigger('remove-node', node); 74 | }); 75 | 76 | mediatorChannel.on('add-link', function(link) { 77 | if (link.target.id) { 78 | dbaas.trigger( 'add-link', link ); 79 | } 80 | }); 81 | 82 | mediatorChannel.on('remove-link', function(link) { 83 | dbaas.trigger('remove-link', link); 84 | }); 85 | 86 | mediatorChannel.on('add-node', function(node, callback) { 87 | dbaas.trigger( 'add-node', node || {}, callback ); 88 | } ); 89 | 90 | mediatorChannel.on('edit-node', function(node) { 91 | dbaas.trigger( 'edit-node', node ); 92 | }); 93 | 94 | dbaas.on( 'node-added', function(node) { 95 | ForceView.trigger('add-node', node ); 96 | ForceView.trigger('remove-node', {}); 97 | } ); 98 | 99 | dbaas.on('node-removed', function(node) { 100 | ForceView.trigger('remove-node', node); 101 | }); 102 | 103 | dbaas.on( 'node-edited', function(node) { 104 | ForceView.trigger('edit-node', node); 105 | } ); 106 | 107 | dbaas.on( 'link-added', function(link) { 108 | ForceView.trigger('add-link', link); 109 | } ); 110 | 111 | dbaas.on( 'link-removed', function(link) { 112 | ForceView.trigger('remove-link', link); 113 | } ); 114 | 115 | dbaas.trigger('retrieve-all-nodes'); 116 | 117 | }); 118 | }, 119 | 120 | syncWithForceView: function() { 121 | var view = this; 122 | 123 | ForceView.on('node-removed', function(node) { 124 | view.mediatorChannel.trigger( 'remove-node', node ); 125 | }); 126 | 127 | ForceView.on('node-edited', function(node) { 128 | view.model.set({ 129 | id: node.id, 130 | color: node.color, 131 | label: node.label 132 | }); 133 | view.openModal(); 134 | }); 135 | 136 | ForceView.on('link-added', function(link) { 137 | view.mediatorChannel.trigger( 'add-link', link ); 138 | }); 139 | 140 | ForceView.on('link-removed', function(link) { 141 | view.mediatorChannel.trigger('remove-link', link); 142 | }); 143 | 144 | ForceView.on('node-and-link-added', function(data) { 145 | view.mediatorChannel.trigger( 'add-node', data.node, function( node ) { 146 | data.node.id = node.id; 147 | ForceView.trigger('link-added', data.link); 148 | } ); 149 | }); 150 | 151 | ForceView.trigger('remove-node', {}); 152 | } 153 | 154 | }); 155 | 156 | }); 157 | -------------------------------------------------------------------------------- /force-view.js: -------------------------------------------------------------------------------- 1 | // http://bl.ocks.org/benzguo/4370043 2 | define(['d3', 'jquery', 'backbone'], function(d3, $, Backbone) { 3 | var width = 960, 4 | height = 500, 5 | fill = d3.scale.category20(), 6 | outer, 7 | vis, 8 | force, 9 | drag_line, 10 | // layout properties 11 | nodes, 12 | links, 13 | node, 14 | link, 15 | text, 16 | // mouse event vars 17 | selected_node, 18 | selected_link, 19 | mousedown_link, 20 | mousedown_node, 21 | mouseup_node, 22 | ForceViewEventChannel = $.extend( {}, Backbone.Events ); 23 | 24 | function init(callback) { 25 | // init svg 26 | outer = d3.select("#chart") 27 | .append("svg:svg") 28 | .attr("width", width) 29 | .attr("height", height) 30 | .attr("pointer-events", "all"); 31 | vis = outer 32 | .append('svg:g') 33 | .call(d3.behavior.zoom().on("zoom", rescale)) 34 | .on("dblclick.zoom", null) 35 | .append('svg:g') 36 | .on("mousemove", mousemove) 37 | .on("mousedown", mousedown) 38 | .on("mouseup", mouseup); 39 | vis.append('svg:rect') 40 | .attr('width', width) 41 | .attr('height', height) 42 | .attr('fill', 'white'); 43 | 44 | // init force layout 45 | force = d3.layout.force() 46 | .size([width, height]) 47 | .nodes([]) 48 | .linkDistance(50) 49 | .charge(-200) 50 | .on("tick", tick); 51 | 52 | // line displayed when dragging new nodes 53 | drag_line = vis.append("line") 54 | .attr("class", "drag_line") 55 | .attr("x1", 0) 56 | .attr("y1", 0) 57 | .attr("x2", 0) 58 | .attr("y2", 0); 59 | 60 | // get layout properties 61 | nodes = force.nodes(); 62 | links = force.links(); 63 | node = vis.selectAll(".node"); 64 | link = vis.selectAll(".link"); 65 | text = vis.selectAll(".text"); 66 | 67 | // add keyboard callback 68 | d3.select(window).on("keydown", keydown); 69 | 70 | redraw(); 71 | callback && callback(); 72 | } 73 | 74 | // focus on svg 75 | // vis.node().focus(); 76 | 77 | function mousedown() { 78 | if (!mousedown_node && !mousedown_link) { 79 | // allow panning if nothing is selected 80 | vis.call(d3.behavior.zoom().on("zoom"), rescale); 81 | return; 82 | } 83 | } 84 | 85 | function mousemove() { 86 | if (!mousedown_node) return; 87 | 88 | // update drag line 89 | drag_line 90 | .attr("x1", mousedown_node.x) 91 | .attr("y1", mousedown_node.y) 92 | .attr("x2", d3.svg.mouse(this)[0]) 93 | .attr("y2", d3.svg.mouse(this)[1]); 94 | 95 | } 96 | 97 | function mouseup() { 98 | if (mousedown_node) { 99 | // hide drag line 100 | drag_line 101 | .attr("class", "drag_line_hidden") 102 | 103 | if (!mouseup_node) { 104 | // add node 105 | var point = d3.mouse(this), 106 | node = {x: point[0], y: point[1]}, 107 | n = nodes.push(node), 108 | link = {source: mousedown_node, target: node}; 109 | 110 | // select new node 111 | selected_node = node; 112 | selected_link = null; 113 | 114 | // add link to mousedown node 115 | links.push(link); 116 | ForceViewEventChannel.trigger('node-and-link-added', { 117 | node: node, 118 | link: link 119 | }); 120 | } 121 | 122 | redraw(); 123 | } 124 | // clear mouse event vars 125 | resetMouseVars(); 126 | } 127 | 128 | function resetMouseVars() { 129 | mousedown_node = null; 130 | mouseup_node = null; 131 | mousedown_link = null; 132 | } 133 | 134 | function tick() { 135 | link.attr("x1", function(d) { return d.source.x; }) 136 | .attr("y1", function(d) { return d.source.y; }) 137 | .attr("x2", function(d) { return d.target.x; }) 138 | .attr("y2", function(d) { return d.target.y; }); 139 | 140 | node.attr("cx", function(d) { return d.x; }) 141 | .attr("cy", function(d) { return d.y; }); 142 | 143 | text.attr("dx", function(d) { return 10 + d.x; }) 144 | .attr("dy", function(d) { return d.y; }) 145 | .attr("fill", function(d) { return d.color ? d.color : 'black'; }) 146 | .text(function(d){return d.label}); 147 | } 148 | 149 | // rescale g 150 | function rescale() { 151 | trans=d3.event.translate; 152 | scale=d3.event.scale; 153 | 154 | vis.attr("transform", 155 | "translate(" + trans + ")" 156 | + " scale(" + scale + ")"); 157 | } 158 | 159 | // redraw force layout 160 | function redraw() { 161 | 162 | link = link.data(links); 163 | 164 | link.enter().insert("line", ".node") 165 | .attr("class", "link") 166 | .on("mousedown", 167 | function(d) { 168 | mousedown_link = d; 169 | if (mousedown_link == selected_link) selected_link = null; 170 | else selected_link = mousedown_link; 171 | selected_node = null; 172 | redraw(); 173 | }) 174 | 175 | link.exit().remove(); 176 | 177 | link 178 | .classed("link_selected", function(d) { return d === selected_link; }); 179 | 180 | node = node.data(nodes); 181 | 182 | node.enter().insert("circle") 183 | .attr("class", "node") 184 | .attr("r", 5) 185 | .on("mousedown", 186 | function(d) { 187 | // disable zoom 188 | vis.call(d3.behavior.zoom().on("zoom"), null); 189 | 190 | mousedown_node = d; 191 | if (mousedown_node == selected_node) selected_node = null; 192 | else selected_node = mousedown_node; 193 | selected_link = null; 194 | 195 | // reposition drag line 196 | drag_line 197 | .attr("class", "link") 198 | .attr("x1", mousedown_node.x) 199 | .attr("y1", mousedown_node.y) 200 | .attr("x2", mousedown_node.x) 201 | .attr("y2", mousedown_node.y); 202 | 203 | redraw(); 204 | }) 205 | .on("mousedrag", 206 | function(d) { 207 | // redraw(); 208 | }) 209 | .on("dblclick", function(d) { 210 | ForceViewEventChannel.trigger('node-edited', d); 211 | }) 212 | .on("mouseup", 213 | function(d) { 214 | if (mousedown_node) { 215 | mouseup_node = d; 216 | if (mouseup_node == mousedown_node) { resetMouseVars(); return; } 217 | 218 | // add link 219 | var link = {source: mousedown_node, target: mouseup_node}; 220 | links.push(link); 221 | ForceViewEventChannel.trigger('link-added', link); 222 | 223 | // select new link 224 | selected_link = link; 225 | selected_node = null; 226 | 227 | // enable zoom 228 | vis.call(d3.behavior.zoom().on("zoom"), rescale); 229 | redraw(); 230 | } 231 | }) 232 | .transition() 233 | .duration(750) 234 | .ease("elastic") 235 | .attr("r", 6.5); 236 | 237 | node.exit().transition() 238 | .attr("r", 0) 239 | .remove(); 240 | 241 | node 242 | .classed("node_selected", function(d) { return d === selected_node; }); 243 | 244 | 245 | text = text.data(nodes); 246 | text.enter().insert("text").attr("class", "text"); 247 | text.exit().remove(); 248 | 249 | 250 | if (d3.event) { 251 | // prevent browser's default behavior 252 | d3.event.preventDefault(); 253 | } 254 | 255 | force.start(); 256 | 257 | } 258 | 259 | function spliceLinksForNode(node) { 260 | var toSplice = links.filter( function(l) { 261 | return (l.source === node) || (l.target === node); 262 | }); 263 | toSplice.map( function(l) { 264 | links.splice(links.indexOf(l), 1); 265 | }); 266 | } 267 | 268 | function keydown() { 269 | if (!selected_node && !selected_link) return; 270 | switch (d3.event.keyCode) { 271 | case 8: // backspace 272 | case 46: { // delete 273 | if (selected_node) { 274 | nodes.splice(nodes.indexOf(selected_node), 1); 275 | spliceLinksForNode(selected_node); 276 | ForceViewEventChannel.trigger('node-removed', selected_node); 277 | } 278 | else if (selected_link) { 279 | links.splice(links.indexOf(selected_link), 1); 280 | ForceViewEventChannel.trigger('link-removed', selected_link); 281 | } 282 | selected_link = null; 283 | selected_node = null; 284 | redraw(); 285 | break; 286 | } 287 | } 288 | } 289 | 290 | function searchNode(node) { 291 | return nodes.filter( function(n) { 292 | return !!(node && node.id == n.id); 293 | })[0]; 294 | }; 295 | 296 | function searchLink(link) { 297 | if (link.source && link.target) { 298 | return links.filter( function(l) { 299 | return (l.source === link.source) && (l.target === link.target); 300 | })[0]; 301 | } else if (link.id) { 302 | return links.filter( function(l) { 303 | return link.id === l.id; 304 | })[0]; 305 | } else { 306 | return undefined; 307 | } 308 | }; 309 | 310 | ForceViewEventChannel.on('clear', function(callback) { 311 | while (nodes.pop()); 312 | while (links.pop()); 313 | redraw(); 314 | callback && callback(); 315 | }); 316 | ForceViewEventChannel.on('remove-node', function(node) { 317 | var selected_node = searchNode(node); 318 | if (selected_node) { 319 | nodes.splice(nodes.indexOf(selected_node), 1); 320 | spliceLinksForNode(selected_node); 321 | redraw(); 322 | } 323 | }); 324 | ForceViewEventChannel.on('remove-link', function(link) { 325 | var source, target, selected_link; 326 | source = searchNode(link.source); 327 | target = searchNode(link.target); 328 | selected_link = searchLink({source: source, target: target, id: link.id}); 329 | if (selected_link) { 330 | links.splice(links.indexOf(selected_link), 1); 331 | redraw(); 332 | } 333 | }); 334 | ForceViewEventChannel.on('add-node', function(node) { 335 | var selected_node = searchNode(node); 336 | if (!selected_node) { 337 | nodes.push(node); 338 | redraw(); 339 | } 340 | }); 341 | ForceViewEventChannel.on('add-link', function(link) { 342 | var source, target, selected_link; 343 | link = link || {}; 344 | source = searchNode(link.source); 345 | target = searchNode(link.target); 346 | selected_link = searchLink({source: source, target: target}); 347 | if (source && target && !selected_link) { 348 | links.push({source:source, target:target, id: link.id}); 349 | redraw(); 350 | } 351 | }); 352 | ForceViewEventChannel.on('edit-node', function(node) { 353 | var selected_node = searchNode(node); 354 | if (selected_node) { 355 | $.extend(selected_node, node); 356 | redraw(); 357 | } 358 | }); 359 | ForceViewEventChannel.on('init', function(cb) { 360 | init(cb); 361 | }); 362 | ForceViewEventChannel.on('redraw', function() { 363 | redraw(); 364 | }); 365 | 366 | return ForceViewEventChannel; 367 | }); 368 | --------------------------------------------------------------------------------