UNPKG

prodio

Version:

Simplified project management

590 lines (556 loc) 17.5 kB
var MindMap = require('../../charts/mindmap.js'); var applyChartConfiguration = require('../../../lib/charts').applyChartConfiguration; var Loader = require('../../../lib/loader'); var support = require('../../../lib/support'); var el = support.el; var MindMapController = function(container, data){ var self = this; self.project_id = data.root._id; self.container = container; self.chart = MindMap(); applyChartConfiguration('mm', container, self.chart, ['width', 'height', 'identity', 'duration', 'style']); if(!data){ try{ var src = container.innerText; if(src){ var f = new Function('return '+src+';'); data = f(); } container.innerHTML = ''; }catch(e){ console.log(e); } } self.bindEvents(container); if(data){ self.update(data, support.paramByName('focus')); } self.dataAttributePrefix = 'mm'; }; MindMapController.prototype.select = function(node){ // Find previously selected, unselect d3.select(".selected").classed("selected", false); // Select current item d3.select(node).classed("selected", true); }; MindMapController.prototype.selectNode = function(target){ var self = this; if(target){ var sel = d3.select(self.container).selectAll('.node'); sel = sel.filter(function(d){return d._id==(target._id||target)}); sel = (sel[0]||[])[0]; if(sel){ self.select(sel); } } }; MindMapController.prototype.bindEvents = function(container){ var self = this; var getDirection = function(data){ if(!data){ return 'root'; } if(data.position){ return data.position; } return getDirection(data.parent); }; var update = function(node){ var id = node._id; self.chart.update(node); setTimeout(function(){ self.selectNode(id); }, self.chart.duration()+100); }; self.chart.click(function(d){ self.select(this); }); Mousetrap.bind('left', function(){ // left key pressed var selection = d3.select(".node.selected")[0][0]; if(selection){ var data = selection.__data__; var dir = getDirection(data); switch(dir){ case('right'): case('root'): self.selectNode(data.parent || data.left[0]); break; case('left'): self.selectNode((data.children||[])[0]); break; default: break; } } return false; }); Mousetrap.bind('right', function(){ // right key pressed var selection = d3.select(".node.selected")[0][0]; if(selection){ var data = selection.__data__; var dir = getDirection(data); switch(dir){ case('left'): case('root'): self.selectNode(data.parent || data.right[0]); break; case('right'): self.selectNode((data.children||[])[0]); break; default: break; } } return false; }); Mousetrap.bind('up', function(){ // up key pressed var selection = d3.select(".node.selected")[0][0]; if(selection){ var data = selection.__data__; var dir = getDirection(data); switch(dir){ case('root'): break; case('left'): case('right'): var p = data.parent, nl = p.children || [], i=1; if(p[dir]){ nl = p[dir]; } l = nl.length; for(; i<l; i++){ if(nl[i]._id === data._id){ self.selectNode(nl[i-1]); break; } } break; } } return false; }); Mousetrap.bind('down', function(){ // down key pressed // up key pressed var selection = d3.select(".node.selected")[0][0]; if(selection){ var data = selection.__data__; var dir = getDirection(data); switch(dir){ case('root'): break; case('left'): case('right'): var p = data.parent, nl = p.children || [], i=0; if(p[dir]){ nl = p[dir]; } l = nl.length; for(; i<l-1; i++){ if(nl[i]._id === data._id){ self.selectNode(nl[i+1]); break; } } break; } } return false; }); Mousetrap.bind('ins', function(){ var selection = d3.select(".node.selected")[0][0]; if(selection){ self.unbindEvents(); var data = selection.__data__; var dir = getDirection(data); var name = alertify.prompt('New name', function(insert, name){ if(insert && name){ if(dir==='root'){ dir = data.right.length>data.left.length?'left':'right'; } var cl = data[dir] || data.children || data._children; if(!cl){ cl = data.children = []; } return Loader.post('/api/v1/project/'+self.project_id+'/item', { data: { parent_id: data._id, name: name, type: 'unknown' } }, function(err, rec){ self.bindEvents(); if(err){ return alertify.error(err.error||err); } cl.push({ name: rec.name, position: rec.dir, _id: rec._id, description: rec.description, rec: rec }); return update(data); }); } if(insert && !name){ alertify.error('Must supply a valid name!'); } self.bindEvents(); }); } }); Mousetrap.bind('del', function(){ var selection = d3.select(".node.selected")[0][0]; if(selection){ var data = selection.__data__; var dir = getDirection(data); var root = data.parent; if(dir==='root'){ alert('Can\'t delete root'); return; } var cl = data.parent[dir] || data.parent.children; if(!cl){ alert('Could not locate children'); return; } var i = 0, l = cl.length; for(; i<l; i++){ if(cl[i]._id === data._id){ self.unbindEvents(); alertify.confirm('Sure you want to delete '+data.name+'?', function(remove){ if(remove){ return Loader.delete('/api/v1/project/'+self.project_id+'/item/'+data._id, function(err, deleted){ self.bindEvents(); if(err){ return alertify.error(err.error||err); } cl.splice(i, 1); update(root); }); } self.bindEvents(); }); break; } } } }); var editNodeName = function(){ var selection = d3.select(".node.selected")[0][0]; if(selection){ self.unbindEvents(); var data = selection.__data__; var name = alertify.prompt('New text:', function(accepted, name){ self.bindEvents(); if(!accepted){ return; } if(name !== data.name){ var rec = data.rec; var uri = data.parent? '/api/v1/project/'+self.project_id+'/item/'+data._id: '/api/v1/project/'+self.project_id; rec.name = name; return Loader.post(uri, { data: rec }, function(err, rec){ self.bindEvents(); if(err){ return alertify.error(err.error||err); } data.name = rec.name; data.rec = rec; update(selection); }); } self.bindEvents(); }, data.name); } }; var editNode = function(){ var selection = d3.select(".node.selected")[0][0]; if(selection){ var data = selection.__data__; if(data.parent){ return window.location.href = '#project/'+self.project_id+'/item/'+data._id; } return window.location.href = '#project/edit/'+self.project_id; } }; Mousetrap.bind('ctrl+enter', function(){ setTimeout(function(){ editNodeName(); }, 100); }); Mousetrap.bind('enter', editNode); self.chart.doubleClick(editNode); Mousetrap.bind('esc', function(){ if(draggingNode){ selectedNode=null; endDrag(); draggingNode=null; } }); var scanChildren = function(node, nodes, exclude){ if((exclude||[]).indexOf(node)===-1){ nodes.push(node); } (node.children||[]).forEach(function(node){ scanChildren(node, nodes); }); return nodes; }; var distance = function(firstObject, secondObject){ var xs = 0; var xy = 0; xs = secondObject.x0 - firstObject.x0; xs = xs * xs; ys = secondObject.y0 - firstObject.y0; ys = ys * ys; return Math.sqrt( xs + ys ); }; var findNearest = function(n, nodes){ var d = Number.MAX_VALUE, t, result; nodes.forEach(function(node){ if(n._id !== node._id){ t = distance(n, node); if(t<d){ result = node; d = t; } } }); return result; }; var nodes, allNodes, dragStarted, draggingNode=null, selectedNode=null, movement; // Define the drag listeners for drag/drop behaviour of nodes. self.dragListener = d3.behavior.drag() .on("dragstart", function(d) { if (!d.parent) { return; } nodes = scanChildren(d, []); allNodes = scanChildren(self.data, [], nodes); movement = { x: 0, y: 0 } dragStarted = true; d3.event.sourceEvent.stopPropagation(); // it's important that we suppress the mouseover event on the node being dragged. Otherwise it will absorb the mouseover event and the underlying node will not detect it d3.select(this).attr('pointer-events', 'none'); }) .on("drag", function(d) { if (!d.parent) { return; } if (dragStarted) { movement.x+=d3.event.dx; movement.y+=d3.event.dy; if(Math.abs(movement.x)<20&&Math.abs(movement.y)<20){ return; } domNode = this; initiateDrag(d, domNode); } selectedNode = findNearest(draggingNode, allNodes); d.x0 += d3.event.dy; d.y0 += d3.event.dx; var node = d3.select(this); node.attr("transform", "translate(" + d.y0 + "," + d.x0 + ")"); updateTempConnector(); }).on("dragend", function(d) { if(!draggingNode){ return; } if (!d.parent) { return; } domNode = this; endDrag(); }); var updateTempConnector = function() { var data = []; if (draggingNode !== null && selectedNode !== null) { // have to flip the source coordinates since we did this for the existing connectors on the original tree data = [{ source: { x: draggingNode.y0, y: draggingNode.x0 }, target: { x: selectedNode.y0, y: selectedNode.x0 } }]; } var link = d3.select(el(self.container, 'svg g')).selectAll(".templink").data(data); link.enter().append("path") .attr("class", "templink") .attr("d", d3.svg.diagonal()) .attr('pointer-events', 'none'); link.attr("d", d3.svg.diagonal()); link.exit().remove(); }; function endDrag() { d3.select(domNode).attr('class', 'node'); updateTempConnector(); d3.select('.templink').remove(); if(draggingNode !== null){ var uri = draggingNode.parent? '/api/v1/project/'+self.project_id+'/item/'+draggingNode._id: '/api/v1/project/'+self.project_id; if(selectedNode){ draggingNode.rec.parent_id = selectedNode.rec._id; draggingNode.parent = selectedNode; return Loader.post(uri, { data: draggingNode.rec }, function(err, rec){ if(!selectedNode.children){ selectedNode.children=selectedNode._children||[]; selectedNode._children=null; } selectedNode.children.push(draggingNode); self.updateNode(selectedNode); draggingNode = null; selectedNode = null; }); } } selectedNode = null; draggingNode.parent.children=draggingNode.parent._children||[]; draggingNode.parent._children=null; draggingNode.parent.children.push(draggingNode); self.updateNode(draggingNode.parent); } function initiateDrag(d, domNode) { draggingNode = d; d3.select(container).selectAll("g.node").sort(function(a, b) { // select the parent and sort the path's if (a._id != draggingNode._id) return 1; // a is not the hovered element, send "a" to the back else return -1; // a is the hovered element, bring "a" to the front }); // if nodes has children, remove the links and nodes if (nodes.length > 1) { // remove link paths links = self.chart.tree.links(nodes); nodePaths = d3.select(container).selectAll("path.link") .data(links, function(d) { return d.target._id; }).remove(); // remove child nodes nodesExit = d3.select(container).selectAll("g.node") .data(nodes, function(d) { return d._id; }).filter(function(d, i) { if (d._id == draggingNode._id) { return false; } return true; }).remove(); } // remove parent link parentLink = self.chart.tree.links(self.chart.tree.nodes(draggingNode.parent)); d3.select(container).selectAll('path.link').filter(function(d, i) { if (d.target._id == draggingNode._id) { return true; } return false; }).remove(); dragStarted = null; } }; MindMapController.prototype.unbindEvents = function(container){ Mousetrap.unbind('left'); Mousetrap.unbind('right'); Mousetrap.unbind('up'); Mousetrap.unbind('down'); Mousetrap.unbind('ins'); Mousetrap.unbind('del'); Mousetrap.unbind('enter'); }; MindMapController.prototype.updateNode = function(data){ var self = this; d3.select(self.container) //.datum(data) .call(self.chart) ; }; MindMapController.prototype.update = function(data, focus){ var self = this; self.data = data = data || self.data; if(!data){ return; } if(data.root && data.nodes && data.edges){ var tree = {}, nodes = {}; tree.name = data.root.name; tree.children = []; tree._id = data.root._id; tree.rec = data.root; tree.description = data.root.description; nodes[data.root._id] = tree; data.nodes.forEach(function(node){ nodes[node._id] = { name: node.name, _id: node._id, rec: node, description: node.description, children: [] }; }); data.edges.forEach(function(edge){ if(nodes[edge.from] && nodes[edge.to]){ return nodes[edge.from].children.push(nodes[edge.to]); } if(!nodes[edge.from]){ var msg = 'Invalid: '+edge.to+' refrences '+edge.from+' that doesn\'t exist!'; console.log(msg); return alertify.error(msg); } var msg = 'Invalid: '+edge.from+' refrences '+edge.to+' that doesn\'t exist!'; console.log(msg); return alertify.error(msg); }); self.data = data = tree; } self.chart.nodeClass(function(d){ return ('node '+(d.rec.status||'').replace(/\s+/g, '-').toLowerCase()).trim(); }); self.chart.nodeEnter(function(node){ node .call(self.dragListener) ; node.append('svg:circle') .attr('r', 1e-6); node.append('svg:text') .attr('text-anchor', 'middle') .attr('dy', 14) .text(function(d){ return d.name; }) .style('fill-opacity', 1); }); d3.select(self.container) .datum(data) .call(self.chart) ; setTimeout(function(){ self.selectNode(focus||data._id); }, self.chart.duration()+100); }; MindMapController.prototype.teardown = function(container){ var self = this; self.unbindEvents(container); self.container = null; }; require('../../../lib/controllers').register('MindMap', MindMapController);