summaryrefslogtreecommitdiff
path: root/src/client/rz_core.js
diff options
context:
space:
mode:
authorAlon Levy <alon@pobox.com>2014-12-16 17:37:42 +0200
committerAlon Levy <alon@pobox.com>2014-12-16 17:37:42 +0200
commitade19de410e0a789a921ec1666b278b2a64176d6 (patch)
treec4c54b106de93609ca7bb70a20fd4be696715dbe /src/client/rz_core.js
parentc7d026b1d504323a8c61d51b726e5e8d2337fc4b (diff)
moving files around after repository merger
Diffstat (limited to 'src/client/rz_core.js')
-rw-r--r--src/client/rz_core.js756
1 files changed, 756 insertions, 0 deletions
diff --git a/src/client/rz_core.js b/src/client/rz_core.js
new file mode 100644
index 00000000..7ec19692
--- /dev/null
+++ b/src/client/rz_core.js
@@ -0,0 +1,756 @@
+"use strict"
+
+define(['jquery', 'd3', 'consts', 'rz_bus', 'util', 'model/graph', 'model/core', 'view/helpers', 'view/view', 'rz_observer', 'view/selection', 'rz_config'],
+function($, d3, consts, rz_bus, util, model_graph, model_core, view_helpers, view, rz_observer, selection, rz_config) {
+
+var addednodes = [],
+ vis,
+ graphinterval = 0,
+ timeline_timer = 0,
+ deliverables = [],
+ circle, // <-- should not be module globals.
+ scrollValue = 0,
+ graph,
+ drag,
+ force;
+
+// "CSS" for SVG elements. Reused for editing elements.
+var node_text_dx = 15,
+ node_text_dy = '.30em',
+ svg_input_fo_node_x = node_text_dx,
+ svg_input_fo_node_y = '-.70em',
+ svg_input_fo_height = '30px';
+
+/**
+ * svgInput - creates an embedded input element under a given
+ *
+ * edit_node(@sibling, @node)
+ * edit_link(@sibling, @link)
+ */
+var svgInput = (function() {
+ var measure_node = $('#measure-node')[0],
+ measure_link = $('#measure-link')[0],
+ original_element,
+ is_link;
+
+ function appendForeignElementInputWithID(base, elemid, width, height)
+ {
+ var input = document.createElement('input'),
+ body = document.createElement('body'),
+ fo = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+
+ body.appendChild(input);
+
+ fo.setAttribute('height', height || svg_input_fo_height);
+ fo.style.pointerEvents = 'none';
+ input.style.pointerEvents = 'all';
+ fo.appendChild(body);
+ base.appendChild(fo);
+ input.setAttribute('id', elemid);
+ return input;
+ }
+
+ function measure(text)
+ {
+ var span;
+
+ span = is_link ? measure_link : measure_node;
+ span.innerHTML = text;
+ return span.getBoundingClientRect().width; // $().width() works too
+ }
+
+ function onkeydown(e) {
+ var ret = undefined,
+ jelement = createOrGetSvgInput(),
+ element = jelement[0],
+ newname = jelement.val(),
+ fo = createOrGetSvgInputFO(),
+ d;
+
+ if (element != this) {
+ console.log('unexpected editname_on_keypress this should be the svg-input element');
+ }
+
+ if (e.which == 13 || e.which == 27) {
+ ret = false;
+ d = jelement.data().d;
+ if (e.which == 13 && newname != d.name) {
+ if (d.hasOwnProperty('__src')) {
+ graph.editLink(d.__src.id, d.__dst.id, newname);
+ } else {
+ graph.editName(d.id, newname);
+ }
+ rz_bus.names.push([newname]);
+ update_view__graph(true);
+ }
+ hide();
+ }
+ rz_bus.ui_key.push({where: consts.KEYSTROKE_WHERE_EDIT_NODE, keys: [e.which]});
+ return ret;
+ };
+
+ function resize_measure(e) {
+ resize(measure($(e.target).val()) + 30);
+ }
+
+ function resize(new_width) {
+ var svg_input = createOrGetSvgInput(),
+ fo = createOrGetSvgInputFO();
+
+ svg_input.css('width', new_width);
+ fo.attr('width', new_width);
+ }
+
+ // FIXME: element being deleted. Some delete is legit - removal of related element. Some isn't (a click).
+ // Instead of investigating (time constraint) reparenting as sibling, and introducing
+ // this function. Cost of creation of element is negligble, it's just ugly..
+ function createOrGetSvgInput()
+ {
+ var svg_input_name = 'svg-input',
+ svg_input_selector = '#' + svg_input_name,
+ svg_input = $(svg_input_selector);
+
+ if (svg_input.length == 0) {
+ console.log('creating new svg-input');
+ svg_input = $(appendForeignElementInputWithID(vis[0][0], svg_input_name));
+ svg_input.on('keydown', onkeydown);
+ svg_input.bind('change keypress', resize_measure);
+ }
+ return svg_input;
+ }
+
+ function createOrGetSvgInputFO()
+ {
+ return createOrGetSvgInput().parent().parent();
+ }
+
+ /*
+ * @param e visual node element
+ * @param n node model object
+ */
+ function enable(e, n) {
+ var oldname = n.name,
+ svg_input = createOrGetSvgInput(),
+ fo = createOrGetSvgInputFO();
+
+ is_link = n.hasOwnProperty('__src');
+
+ e.parentNode.appendChild(fo[0]); // This will unparent from the old parent
+ if (is_link) {
+ fo.attr('transform', e.getAttribute('transform'));
+ // XXX links set the text-anchor middle attribute. no idea how to do that
+ fo.attr('x', -$(e).width() / 2);
+ fo.attr('y', -$(e).height() / 2 - 3); // XXX This minus 3 is only kinda ok.
+ fo.attr('class', 'svg-input-fo-link');
+ } else {
+ fo.attr('x', svg_input_fo_node_x);
+ fo.attr('y', svg_input_fo_node_y);
+ fo.attr('transform', null);
+ fo.attr('class', 'svg-input-fo-node');
+ }
+ // Set width correctly
+ resize(measure(oldname) + 30);
+ fo.show();
+ svg_input.val(oldname);
+ svg_input.data().d = n;
+ svg_input.focus();
+ if (original_element) {
+ original_element.show();
+ }
+ original_element = $(e);
+ original_element.hide();
+ // TODO: set cursor to correct location in text
+ }
+
+ function hide() {
+ createOrGetSvgInputFO().hide();
+ if (original_element && original_element.show) {
+ original_element.show();
+ }
+ }
+
+ return {
+ enable: enable,
+ hide: hide,
+ };
+}());
+
+
+
+function recenterZoom() {
+ vis.attr("transform", "translate(0,0)scale(1)");
+}
+
+// zoom or drag
+var zoomInProgress = false;
+
+var initDrawingArea = function () {
+
+ function zoom() {
+ zoomInProgress = true;
+ vis.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
+ d3.event.sourceEvent.stopPropagation();
+ }
+
+ function dragstarted(d) {
+ d3.event.sourceEvent.stopPropagation();
+ d3.select(this).classed("dragging", true);
+ d.dragstart = {clientX:d3.event.sourceEvent.clientX, clientY:d3.event.sourceEvent.clientY};
+ force.stop();
+ }
+
+ function dragged(d) {
+ d.x = d3.event.x;
+ d.y = d3.event.y;
+ tick();
+ }
+
+ function dragended(d) {
+ d3.select(this).classed("dragging", false);
+ d3.select(this).classed("fixed", true); // TODO: this is broken since we override all the classes. Need to switch to class addition/removal (i.e. use classed for everything) or set class in one location (so here just set a value on the node, not the element)
+ if (d.dragstart.clientX - d3.event.sourceEvent.clientX != 0 ||
+ d.dragstart.clientY - d3.event.sourceEvent.clientY != 0) {
+ tick();
+ force.resume();
+ }
+ }
+
+ graph = new model_graph.Graph();
+
+ var user_id = $('#user_id'),
+ user = user_id.text();
+
+ if (user_id.length > 0) {
+ console.log('found user ID: \'' + user + '\'');
+ graph.set_user(user);
+ }
+
+ var el = document.body;
+ vis = d3.select(el).append("svg:svg")
+ .attr('id', 'canvas_d3')
+ .attr("width", '100%')
+ .attr("height", '100%')
+ .attr("pointer-events", "all")
+ .append("g")
+ .attr("class", "zoom");
+
+ d3.select(el).select("svg").append("svg:defs")
+ .data(["end"]) // Different link/path types can be defined here
+ .append("svg:marker") // This section adds in the arrows
+ .attr("id", String)
+ .attr("viewBox", "0 -5 10 10")
+ .attr("refX", 22)
+ .attr("refY", -1.5)
+ .attr("markerWidth", 4)
+ .attr("markerHeight", 4)
+ .attr("orient", "auto")
+ .append("svg:path")
+ .attr("d", "M0,-5L10,0L0,5");
+
+ /*
+ * init zoom behavior
+ */
+ var zoom_obj = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
+ zoom_obj(d3.select('#canvas_d3'))
+ d3.select("svg").on("dblclick.zoom", null); // disable zoom on double click
+
+ $('svg').click(svg_click_handler);
+
+ // SVG rendering order is last rendered on top, so to make sure
+ // all links are below the nodes we group them under a single g
+ vis.append("g").attr("id", "link-group");
+ vis.append("g").attr("id", "selected-link-group");
+
+ drag = d3.behavior.drag()
+ .origin(function(d) { return d; })
+ .on("dragstart", dragstarted)
+ .on("drag", dragged)
+ .on("dragend", dragended);
+
+ // $('#canvas_d3').dblclick(canvas_handler_dblclick); - see #138
+ if (rz_config.backend_enabled){
+ graph.load_from_backend( function(){
+ update_view__graph(false);
+ });
+ }
+}
+
+function init_force_layout(){
+ var el = document.body;
+ var w = $(el).innerWidth(),
+ h = $(el).innerHeight();
+
+ force = d3.layout.force()
+ .distance(120)
+ .gravity(0.12)
+ .charge(-1800)
+ .size([w, h])
+ .on("tick", tick)
+ .start();
+}
+
+initDrawingArea();
+init_force_layout();
+
+/**
+ * find the visual element counterpart of a given model object. This relies on
+ * the visual element having an id attribute equal to the object's id.
+ *
+ * @return null if visual element is not found
+ */
+function locate_visual_element(model_obj){
+ var id_sel = $('#' + model_obj.id);
+ if (0 == id_sel.length){
+ console.warn('unable to find visual element for model object: object id: ' + model_obj.id.toString())
+ return null;
+ }
+ return id_sel[0];
+}
+
+/**
+ * add node on canvas double click
+ */
+function canvas_handler_dblclick(){
+ var n = model_core.create_node__set_random_id();
+ n.name = ''; // will be set by user
+
+ graph.addNode(n);
+ update_view__graph();
+
+ var n_ve = locate_visual_element(n); // locate visual element
+
+ var on_slowdown_cb = function(){
+ svgInput.enable($(n_ve).find('.nodetext'), n);
+ observer.disconnect();
+ }
+ var mutation_handler = rz_observer.new_Mutation_Handler__on_dxy_slowdown(on_slowdown_cb);
+ var observer = rz_observer.new_MutationObserver(mutation_handler);
+ mutation_handler.on_slowdown_threshold_reached;
+
+ observer.observe(n_ve, {
+ subtree: false,
+ childList : false,
+ attributes: true,
+ attributeOldValue : true,
+ });
+}
+
+/**
+ * update view: graph
+ */
+function update_view__graph(no_relayout) {
+ var node,
+ link,
+ link_g,
+ linktext,
+ nodetext,
+ unselected_link_group = document.querySelector('#link-group'),
+ selected_link_group = document.querySelector('#selected-link-group');
+
+ link = vis.selectAll("g.link")
+ .data(graph.links(), function(d) { return d.id; });
+
+ link_g = link.enter().append('g')
+ .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping
+ .attr('class', 'link graph')
+
+ link_g.append("path")
+ .attr("class", function(d) {
+ return d.state + ' link graph';
+ })
+ .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping
+ .attr("marker-end", "url(#end)");
+
+ // second path for larger click area
+ link_g.append("path")
+ .attr("class", "ghostlink")
+ .on("click", function(d, i) {
+ if (zoomInProgress) {
+ // don't disable zoomInProgress, it will be disabled by the svg_click_handler
+ // after this events bubbles to the svg element
+ return;
+ }
+ var that = this,
+ src = this.link.__src,
+ dst = this.link.__dst;
+
+ view.edge_info.on_delete(function () {
+ graph.removeLink(that.link);
+ update_view__graph(true);
+ view.edge_info.hide();
+ });
+ view.edge_info.show(d);
+ selection.update([src, dst]);
+ update_view__graph(true);
+ });
+
+ link.attr("class", function(d, i){
+ var temp_and = (d.name && d.name.replace(/ /g,"")=="and" && d.state==="temp") ? "temp_and" : "";
+
+ return ["graph link", temp_and, selection.selected_class(d)].join(' ');
+ });
+
+ link.selectAll('path.link')
+ .attr('class', function(d) {
+ return [d.state, selection.selected_class(d), "link graph"].join(' ');
+ });
+
+ link.exit().remove();
+
+ vis.selectAll('.ghostlink')
+ .data(graph.links())
+ .each(function (d) {
+ this.link = d;
+ });
+
+ linktext = vis.selectAll(".linklabel")
+ .data(graph.links(), function(d) { return d.id; });
+ linktext.enter()
+ .append("text")
+ .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping
+ .attr("class", function(d) {
+ return ["linklabel graph", selection.selected_class(d)].join(' ');
+ })
+ .attr("text-anchor", "middle")
+ .on("click", function(d, i) {
+ if (d.state !== "temp") {
+ svgInput.enable(this, d);
+ }
+ });
+
+ linktext
+ .text(function(d) {
+ var name = d.name || "";
+ if (!(d.__dst.state === "temp" ||
+ d.__src.state === "chosen" || d.__dst.state === "chosen")) {
+ return "";
+ }
+ if (name.length < 25 || d.__src.state === "chosen" ||
+ d.__dst.state === "chosen" || d.state==="temp") {
+ return name;
+ } else {
+ return name.substring(0, 14) + "...";
+ }
+ });
+
+ linktext.exit().remove();
+
+ node = vis.selectAll(".node")
+ .data(graph.nodes(), function(d) {
+ return d.id;
+ });
+
+ var nodeEnter = node.enter()
+ .append("g")
+ .attr('id', function(d){ return d.id; }) // append node id to enable data->visual mapping
+ .attr('visibility', 'hidden') // made visible on first tick
+ .call(drag);
+
+ // reorder nodes so selected are last, and so rendered last, and so on top.
+ (function () {
+ var ontop = [],
+ bubble;
+
+ node.each(function (d) {
+ this.node = d;
+ })
+ .attr('class', function(d) {
+ if (selection.node_selected(d)) {
+ if (d.type == 'bubble') {
+ bubble = this;
+ } else {
+ ontop.push(this);
+ }
+ }
+ return ['node', selection.selected_class(d)].join(' ');
+ });
+ if (bubble === undefined) {
+ // nothing to do if there is no bubble
+ return;
+ }
+ function reparent(new_parent, element) {
+ if (element.parentNode == new_parent) {
+ return;
+ }
+ new_parent.appendChild(element);
+ }
+ // move link to correct group
+ // O(|links|*|ontop|)
+ link.each(function (d) {
+ if (ontop.some(function (node) {
+ var d_node = node.node;
+ return d.__src == d_node || d.__dst == d_node;
+ }))
+ {
+ reparent(selected_link_group, this);
+ } else {
+ reparent(unselected_link_group, this);
+ }
+ });
+ linktext.each(function (d) {
+ if (selection.node_selected(d)) {
+ ontop.push(this);
+ }
+ });
+ function moveToEnd(e) {
+ e.parentNode.appendChild(e);
+ }
+ moveToEnd(bubble);
+ ontop.reverse().forEach(function (e) {
+ moveToEnd(e);
+ });
+ var count_links = function() {
+ return selected_link_group.childElementCount + unselected_link_group.childElementCount;
+ };
+ // put back on top link group on top
+ selected_link_group.parentNode.insertBefore(selected_link_group, bubble.nextSibling);
+ })();
+
+ nodetext = nodeEnter.insert("text")
+ .attr("class", "nodetext graph")
+ .attr("dx", node_text_dx)
+ .attr("dy", node_text_dy)
+ .on("click", function(d, i) {
+ if (d3.event.defaultPrevented) {
+ // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on
+ return;
+ }
+ if (d.state !== "temp") {
+ svgInput.enable(this, d);
+ selection.update([d]);
+ showNodeInfo(this.parentNode.node, i);
+ }
+ d3.event.stopPropagation();
+ });
+
+ node.select('g.node text')
+ .text(function(d) {
+ if (!d.name) {
+ return d.type == 'bubble' ? "" : "_";
+ }
+ if (d.state === "temp" || d.state === 'chosen'
+ || d.state === "enter" || d.state === "exit") {
+ return d.name;
+ } else {
+ if (d.name.length < 28) {
+ return d.name;
+ } else {
+ return d.name.substring(0, 25) + "...";
+ }
+ }
+ });
+
+ circle = nodeEnter.insert("circle");
+ node.select('g.node circle')
+ .attr("class", function(d) {
+ return d.type + " " + d.state + " circle graph";
+ })
+ .attr("r", function(d) {
+ return view_helpers.customSize(d.type) - 2;
+ })
+ .on("click", function(d, i) {
+ if (d3.event.defaultPrevented) {
+ // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on
+ return;
+ }
+ d3.event.stopPropagation();
+ selection.update([d]);
+ if(d.state !== "temp") {
+ showNodeInfo(d, i);
+ }
+ });
+ circle.append("svg:image")
+ .attr("class", "status graph")
+ .attr('x', -7)
+ .attr('y', -8)
+ .attr('width', 15)
+ .attr('height', 15)
+ .attr("xlink:href", function(d) {
+ switch (d.status) {
+ case "done":
+ return "res/img/check.png";
+ break;
+ case "current":
+ return "res/img/wait.png";
+ break;
+ case "waiting":
+ return "res/img/cross.png";
+ break;
+ }
+ });
+
+ node.exit().remove();
+
+ //update deliverables
+ deliverables = [];
+ var nodes = graph.nodes();
+ for (var i = 0; i < nodes.length; i++) {
+ var current = nodes[i];
+ if (current.type === "third-internship-proposal") {
+ deliverables.push({
+ "id": nodes[i].id,
+ "startdate": nodes[i].start,
+ "enddate": nodes[i].end
+ });
+ }
+ //Do something
+ }
+
+ force.nodes(graph.nodes())
+ .links(graph.links())
+
+ if (no_relayout) {
+ // XXX If we are stopped we need to update the text of the links at least,
+ // and this is the simplest way
+ tick();
+ } else {
+ force.alpha(0.1).start();
+ }
+}
+
+
+
+var debug_print = function(message) {
+ var element = $(".debug");
+ if (element.length == 1) {
+ element.html(message);
+ } else {
+ console.log(message);
+ }
+}
+
+function check_for_nan(x) {
+ if (Number.isNaN(x)) {
+ console.log('nan problem');
+ force.stop();
+ }
+ return Number.isNaN(x);
+}
+
+var newnodes=1;
+function tick(e) {
+ //console.log(e);
+ //$(".debug").html(force.alpha());
+ var node = vis.selectAll(".node")
+ .data(force.nodes(), function(d) {
+ return d.id;
+ });
+ var link = vis.selectAll("path.link")
+ .data(graph.links(), function(d) {
+ return d.id;
+ });
+ var linktext = vis.selectAll(".linklabel").data(graph.links());
+
+ function transform(d) {
+ if (check_for_nan(d.x) || check_for_nan(d.y)) {
+ return;
+ }
+ return "translate(" + d.x + "," + d.y + ")";
+ }
+
+ //circles animation
+ var tempcounter = 0,
+ temptotal = graph.nodes().filter(function(d){
+ return d.state === "temp" && d.type !== "chainlink" && d.type !== "bubble";
+ }).length;
+ if (temptotal !== newnodes) {
+ newnodes += temptotal / 15 / (newnodes * newnodes);
+ }
+ newnodes = Math.max(1, Math.min(newnodes, temptotal));
+ graph.nodes().forEach(function(d, i) {
+ var r, a;
+ if (d.state === "temp") {
+ tempcounter++;
+ if (d.type==="chainlink" || d.type==="bubble") {
+ d.x = window.innerWidth / 2;
+ d.y = window.innerHeight / 2;
+ } else {
+ r = 60 + newnodes * 20;
+ a = -Math.PI + Math.PI * 2 * (tempcounter-1) / newnodes + 0.3;
+ d.x = window.innerWidth / 2 + r * Math.cos(a);
+ d.y = window.innerHeight / 2 + r * Math.sin(a);
+ }
+ check_for_nan(d.x);
+ check_for_nan(d.y);
+ }
+ });
+
+ link.attr("d", function(d, i) {
+ var d_val,
+ ghost;
+
+ var dx = d.__dst.x - d.__src.x,
+ dy = d.__dst.y - d.__src.y,
+ dr = Math.sqrt(dx * dx + dy * dy);
+ d_val = "M" + d.__src.x + "," + d.__src.y + "L" + d.__dst.x + "," + d.__dst.y;
+ // update ghostlink position
+ ghost = $(this.nextElementSibling);
+ ghost.attr("d", d_val);
+ return d_val;
+ });
+
+ linktext.attr("transform", function(d) {
+ return "translate(" + (d.__src.x + d.__dst.x) / 2 + "," + (d.__src.y + d.__dst.y) / 2 + ")";
+ });
+
+ node.attr("transform", transform);
+
+ // After initial placement we can make the nodes visible.
+ //links.attr('visibility', 'visible');
+ node.attr('visibility', 'visible');
+}
+
+function showNodeInfo(d, i) {
+ view.node_info.on_save(function(e, form_data) {
+
+ graph.update_node(d, form_data, function(){
+ var old_type = d.type,
+ new_type = form_data.type;
+
+ if (new_type != old_type) {
+ view.node_info.show(d);
+ }
+
+ view.node_info.hide();
+ update_view__graph(true);
+ });
+
+ return false;
+ });
+
+ view.node_info.on_delete(function() {
+ if (confirm('This node and all its connections will be deleted, are you sure?')) {
+ graph.removeNode(d.id);
+ update_view__graph(false);
+ view.node_info.hide();
+ }
+ });
+
+ view.node_info.show(d);
+}
+
+function svg_click_handler(e) {
+ if (zoomInProgress) {
+ zoomInProgress = false;
+ return;
+ }
+ if (e.originalEvent.target.nodeName != 'svg') {
+ return;
+ }
+ svgInput.hide();
+ selection.clear();
+ view.hide();
+ update_view__graph(true);
+}
+
+return {
+ graph: graph,
+ force: force,
+ load_from_json: function(result) {
+ graph.load_from_json(result);
+ recenterZoom();
+ update_view__graph(false);
+ },
+ update_view__graph : update_view__graph,
+}
+}); /* close define call */