Commit a6cef4a8 authored by Jérome Perrin's avatar Jérome Perrin

graph_editor: comply with jslint 2014-04-21

Changes:
 * indentation and whitespaces between function and (. ( tips: use ignore whitespace for review)
 * usage of `new`. Some RSVP.Promise where not using `new`, some RSVP.all had an extra `new`
 * fix a "weird condition", javascript does not evaluate (a <= b <= c) same as python
 * change `var` declarations one per line, to be compatible with
   upcoming jslint version. Use a vars: true  directive so that jslint 2014-04-21 is also happy  (
   see https://stackoverflow.com/questions/34862541/expected-and-instead-saw-jslint-multivar-setting )
 * in function definitions: rename unused arguments as `ignore` or remove them when possible ( when they were last )
 * use named functions ( for exemple fillDialog ), otherwise jslint complain they are not in scope.
parent 61629778
/* =========================================================================== /* ===========================================================================
* Copyright 2013-2015 Nexedi SA and Contributors * Copyright 2013-2015 Nexedi SA and Contributors
* *
* This file is part of DREAM. * This file is part of DREAM.
* *
* DREAM is free software: you can redistribute it and/or modify * DREAM is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by * it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* DREAM is distributed in the hope that it will be useful, * DREAM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details. * GNU Lesser General Public License for more details.
* *
* You should have received a copy of the GNU Lesser General Public License * You should have received a copy of the GNU Lesser General Public License
* along with DREAM. If not, see <http://www.gnu.org/licenses/>. * along with DREAM. If not, see <http://www.gnu.org/licenses/>.
* ==========================================================================*/ * ==========================================================================*/
/*global console, window, Node, RSVP, rJS, $, jsPlumb, Handlebars, /*global console, window, Node, RSVP, rJS, $, jsPlumb, Handlebars,
loopEventListener, promiseEventListener, DOMParser, Springy */ loopEventListener, promiseEventListener, DOMParser, Springy */
/*jslint unparam: true todo: true */ /*jslint vars: true unparam: true nomen: true todo: true */
(function(RSVP, rJS, $, jsPlumb, Handlebars, loopEventListener, promiseEventListener, DOMParser, Springy) { (function (RSVP, rJS, $, jsPlumb, Handlebars, loopEventListener, promiseEventListener, DOMParser, Springy) {
"use strict"; "use strict";
/* TODO: /* TODO:
* less dependancies ( promise event listner ? ) * less dependancies ( promise event listener ? )
* no more handlebars * no more handlebars
* id should not always be modifiable * id should not always be modifiable
* drop zoom level * drop zoom level
* rename draggable() * rename draggable()
* factorize node & edge popup edition * factorize node & edge popup edition
*/ */
/*jslint nomen: true */ var gadget_klass = rJS(window);
var gadget_klass = rJS(window), var domParser = new DOMParser();
domParser = new DOMParser(), var node_template_source = gadget_klass.__template_element.getElementById("node-template").innerHTML;
node_template_source = gadget_klass.__template_element.getElementById("node-template").innerHTML, var node_template = Handlebars.compile(node_template_source);
node_template = Handlebars.compile(node_template_source), var popup_edit_template = gadget_klass.__template_element.getElementById("popup-edit-template").innerHTML;
popup_edit_template = gadget_klass.__template_element.getElementById("popup-edit-template").innerHTML;
function layoutGraph(graph_data) {
function layoutGraph(graph_data) { // Promise returning the graph once springy calculated the layout.
// Promise returning the graph once springy calculated the layout. // If the graph already contain layout, return it as is.
// If the graph already contain layout, return it as is. function resolver(resolve, reject) {
function resolver(resolve, reject) { try {
try { var springy_graph = new Springy.Graph();
var springy_graph = new Springy.Graph(), var max_iterations = 100; // we stop layout after 100 iterations.
max_iterations = 100, // we stop layout after 100 iterations. var loop = 0;
loop = 0, var springy_nodes = {};
springy_nodes = {}, var drawn_nodes = {};
drawn_nodes = {}, var min_x = 100;
min_x=100, max_x=0, min_y=100, max_y=0; var max_x = 0;
// if graph is empty, no need to layout var min_y = 100;
if (Object.keys(graph_data.edge).length === 0) { var max_y = 0;
resolve(graph_data); // if graph is empty, no need to layout
if (Object.keys(graph_data.edge).length === 0) {
resolve(graph_data);
return;
}
// make a Springy graph with our graph
$.each(graph_data.node, function (key, value) {
if (value.coordinate && value.coordinate.top && value.coordinate.left) {
// graph already has a layout, no need to layout again
resolve(graph_data);
return;
}
springy_nodes[key] = springy_graph.newNode({node_id: key});
});
$.each(graph_data.edge, function (ignore, value) {
springy_graph.newEdge(springy_nodes[value.source], springy_nodes[value.destination]);
});
var layout = new Springy.Layout.ForceDirected(springy_graph, 400.0, 400.0, 0.5);
var renderer;
renderer = new Springy.Renderer(
layout,
function clear() {
return;
},
function drawEdge() {
return;
},
function drawNode(node, p) {
drawn_nodes[node.data.node_id] = p;
loop += 1;
if (loop > max_iterations) {
renderer.stop();
}
},
function onRenderStop() {
// calculate the min and max of x and y
$.each(graph_data.node, function (key) {
if (drawn_nodes[key].x > max_x) {
max_x = drawn_nodes[key].x;
}
if (drawn_nodes[key].x < min_x) {
min_x = drawn_nodes[key].x;
}
if (drawn_nodes[key].y > max_y) {
max_y = drawn_nodes[key].y;
}
if (drawn_nodes[key].y < min_y) {
min_y = drawn_nodes[key].y;
}
});
// "resample" the positions from 0 to 1, the scale used by this gadget.
// We keep a 5% margin
$.each(graph_data.node, function (key) {
graph_data.node[key].coordinate = {
left: 0.05 + 0.9 * (drawn_nodes[key].x - min_x) / (max_x - min_x),
top: 0.05 + 0.9 * (drawn_nodes[key].y - min_y) / (max_y - min_y)
};
});
resolve(graph_data);
}
);
renderer.start();
} catch (e) {
reject(e);
}
}
return new RSVP.Promise(resolver);
}
function loopJsplumbBind(gadget, type, callback) {
//////////////////////////
// Infinite event listener (promise is never resolved)
// eventListener is removed when promise is cancelled/rejected
//////////////////////////
var handle_event_callback;
var callback_promise;
var jsplumb_instance = gadget.props.jsplumb_instance;
function cancelResolver() {
if (callback_promise !== undefined && typeof callback_promise.cancel === "function") {
callback_promise.cancel();
}
}
function canceller() {
if (handle_event_callback !== undefined) {
jsplumb_instance.unbind(type);
}
cancelResolver();
}
function resolver(ignore, reject) {
handle_event_callback = function () {
var args = arguments;
cancelResolver();
callback_promise = new RSVP.Queue().push(function () {
return callback.apply(jsplumb_instance, args);
}).push(undefined, function (error) {
if (!(error instanceof RSVP.CancellationError)) {
canceller();
reject(error);
}
});
};
jsplumb_instance.bind(type, handle_event_callback);
}
return new RSVP.Promise(resolver, canceller);
}
function getNodeId(gadget, element_id) {
// returns the ID of the node in the graph from its DOM element id
var node_id;
$.each(gadget.props.node_id_to_dom_element_id, function (k, v) {
if (v === element_id) {
node_id = k;
return false;
}
});
return node_id;
}
function generateNodeId(gadget, element) {
// Generate a node id
var n = 1;
var class_def = gadget.props.data.class_definition[element._class];
var id = class_def.short_id || element._class;
while (gadget.props.data.graph.node[id + n] !== undefined) {
n += 1;
}
return id + n;
}
function generateDomElementId(gadget_element) {
// Generate a probably unique DOM element ID.
var n = 1;
while ($(gadget_element).find("#DreamNode_" + n).length > 0) {
n += 1;
}
return "DreamNode_" + n;
}
function getDefaultEdgeClass(gadget) {
var class_definition = gadget.props.data.class_definition;
var key;
for (key in class_definition) {
if (class_definition.hasOwnProperty(key) && class_definition[key]._class === "edge") {
return key;
}
}
return "Dream.Edge";
}
function updateConnectionData(gadget, connection, remove) {
if (connection.ignoreEvent) {
// this hack is for edge edition. Maybe there I missed one thing and
// there is a better way.
return; return;
} }
// make a Springy graph with our graph if (remove) {
$.each(graph_data.node, function(key, value) { delete gadget.props.data.graph.edge[connection.id];
if (value.coordinate && value.coordinate.top && value.coordinate.left) { } else {
// graph already has a layout, no need to layout again var edge_data = gadget.props.data.graph.edge[connection.id] || {
resolve(graph_data); _class: getDefaultEdgeClass(gadget)
return; };
} edge_data.source = getNodeId(gadget, connection.sourceId);
springy_nodes[key] = springy_graph.newNode({node_id: key}); edge_data.destination = getNodeId(gadget, connection.targetId);
}); gadget.props.data.graph.edge[connection.id] = edge_data;
$.each(graph_data.edge, function(key, value) { }
springy_graph.newEdge( gadget.notifyDataChanged();
springy_nodes[value.source], }
springy_nodes[value.destination]);
});
var layout = new Springy.Layout.ForceDirected(springy_graph, 400.0, 400.0, 0.5); function convertToAbsolutePosition(gadget, x, y) {
var renderer = new Springy.Renderer( var zoom_level = gadget.props.zoom_level;
layout, var canvas_size_x = $(gadget.props.main).width();
function clear() {}, var canvas_size_y = $(gadget.props.main).height();
function drawEdge(edge, p1, p2) {}, var size_x = $(gadget.props.element).find(".dummy_window").width() * zoom_level;
function drawNode(node, p) { var size_y = $(gadget.props.element).find(".dummy_window").height() * zoom_level;
drawn_nodes[node.data.node_id] = p; var top = Math.floor(y * (canvas_size_y - size_y)) + "px";
if ( ++loop > max_iterations) { var left = Math.floor(x * (canvas_size_x - size_x)) + "px";
renderer.stop(); return [left, top];
} }
},
function onRenderStop() { function convertToRelativePosition(gadget, x, y) {
// calculate the min and max of x and y var zoom_level = gadget.props.zoom_level;
$.each(graph_data.node, function(key, value) { var canvas_size_x = $(gadget.props.main).width();
if (drawn_nodes[key].x > max_x) { var canvas_size_y = $(gadget.props.main).height();
max_x = drawn_nodes[key].x; var size_x = $(gadget.props.element).find(".dummy_window").width() * zoom_level;
} var size_y = $(gadget.props.element).find(".dummy_window").height() * zoom_level;
if (drawn_nodes[key].x < min_x) { var top = Math.max(Math.min(y.replace("px", "") / (canvas_size_y - size_y), 1), 0);
min_x = drawn_nodes[key].x; var left = Math.max(Math.min(x.replace("px", "") / (canvas_size_x - size_x), 1), 0);
} return [left, top];
if (drawn_nodes[key].y > max_y) { }
max_y = drawn_nodes[key].y;
} function updateElementCoordinate(gadget, node_id, coordinate) {
if (drawn_nodes[key].y < min_y) { var element_id = gadget.props.node_id_to_dom_element_id[node_id];
min_y = drawn_nodes[key].y; var element;
} var relative_position;
}); if (coordinate === undefined) {
// "resample" the positions from 0 to 1, the scale used by this gadget. element = $(gadget.props.element).find("#" + element_id);
// We keep a 5% margin relative_position = convertToRelativePosition(gadget, element.css("left"), element.css("top"));
$.each(graph_data.node, function(key, value) { coordinate = {
graph_data.node[key].coordinate = { left: relative_position[0],
left: 0.05 + 0.9 * (drawn_nodes[key].x - min_x) / (max_x - min_x), top: relative_position[1]
top: 0.05 + 0.9 * (drawn_nodes[key].y - min_y) / (max_y - min_y) };
};
});
resolve(graph_data);
}
);
renderer.start();
} catch (e) {
reject(e);
}
}
return new RSVP.Promise(resolver);
}
function loopJsplumbBind(gadget, type, callback) {
//////////////////////////
// Infinite event listener (promise is never resolved)
// eventListener is removed when promise is cancelled/rejected
//////////////////////////
var handle_event_callback, callback_promise, jsplumb_instance = gadget.props.jsplumb_instance;
function cancelResolver() {
if (callback_promise !== undefined && typeof callback_promise.cancel === "function") {
callback_promise.cancel();
}
}
function canceller() {
if (handle_event_callback !== undefined) {
jsplumb_instance.unbind(type);
}
cancelResolver();
}
function resolver(resolve, reject) {
handle_event_callback = function() {
var args = arguments;
cancelResolver();
callback_promise = new RSVP.Queue().push(function() {
return callback.apply(jsplumb_instance, args);
}).push(undefined, function(error) {
if (!(error instanceof RSVP.CancellationError)) {
canceller();
reject(error);
}
});
};
jsplumb_instance.bind(type, handle_event_callback);
}
return new RSVP.Promise(resolver, canceller);
}
function getNodeId(gadget, element_id) {
// returns the ID of the node in the graph from its DOM element id
var node_id;
$.each(gadget.props.node_id_to_dom_element_id, function(k, v) {
if (v === element_id) {
node_id = k;
return false;
}
});
return node_id;
}
function generateNodeId(gadget, element) {
// Generate a node id
var n = 1,
class_def = gadget.props.data.class_definition[element._class],
id = class_def.short_id || element._class;
while (gadget.props.data.graph.node[id + n] !== undefined) {
n += 1;
}
return id + n;
}
function generateDomElementId(gadget_element) {
// Generate a probably unique DOM element ID.
var n = 1;
while ($(gadget_element).find("#DreamNode_" + n).length > 0) {
n += 1;
}
return "DreamNode_" + n;
}
function getDefaultEdgeClass(gadget) {
var class_definition = gadget.props.data.class_definition;
for (var key in class_definition) {
if (class_definition.hasOwnProperty(key) && class_definition[key]._class === 'edge') {
return key;
}
}
return "Dream.Edge";
}
function updateConnectionData(gadget, connection, remove) {
if (connection.ignoreEvent) {
// this hack is for edge edition. Maybe there I missed one thing and
// there is a better way.
return;
}
if (remove) {
delete gadget.props.data.graph.edge[connection.id];
} else {
var edge_data = gadget.props.data.graph.edge[connection.id] || {
_class: getDefaultEdgeClass(gadget)
};
edge_data.source = getNodeId(gadget, connection.sourceId);
edge_data.destination = getNodeId(gadget, connection.targetId);
gadget.props.data.graph.edge[connection.id] = edge_data;
}
gadget.notifyDataChanged();
}
function convertToAbsolutePosition(gadget, x, y) {
var zoom_level = gadget.props.zoom_level,
canvas_size_x = $(gadget.props.main).width(),
canvas_size_y = $(gadget.props.main).height(),
size_x = $(gadget.props.element).find(".dummy_window").width() * zoom_level,
size_y = $(gadget.props.element).find(".dummy_window").height() * zoom_level,
top = Math.floor(y * (canvas_size_y - size_y)) + "px",
left = Math.floor(x * (canvas_size_x - size_x)) + "px";
return [left, top];
}
function convertToRelativePosition(gadget, x, y) {
var zoom_level = gadget.props.zoom_level,
canvas_size_x = $(gadget.props.main).width(),
canvas_size_y = $(gadget.props.main).height(),
size_x = $(gadget.props.element).find(".dummy_window").width() * zoom_level,
size_y = $(gadget.props.element).find(".dummy_window").height() * zoom_level,
top = Math.max(Math.min(y.replace("px", "") / (canvas_size_y - size_y), 1), 0),
left = Math.max(Math.min(x.replace("px", "") / (canvas_size_x - size_x), 1), 0);
return [left, top];
}
function updateElementCoordinate(gadget, node_id, coordinate) {
var element_id = gadget.props.node_id_to_dom_element_id[node_id],
element,
relative_position;
if (coordinate === undefined) {
element = $(gadget.props.element).find("#" + element_id);
relative_position = convertToRelativePosition(gadget, element.css("left"), element.css("top"));
coordinate = {
left: relative_position[0],
top: relative_position[1]
};
}
gadget.props.data.graph.node[node_id].coordinate = coordinate;
gadget.notifyDataChanged();
return coordinate;
}
function draggable(gadget) {
var jsplumb_instance = gadget.props.jsplumb_instance,
stop = function(element) {
updateElementCoordinate(gadget, getNodeId(gadget, element.target.id));
};
// XXX This function should only touch the node element that we just added.
jsplumb_instance.draggable(jsplumb_instance.getSelector(".window"), {
containment: "parent",
grid: [10, 10],
stop: stop
});
jsplumb_instance.makeSource(jsplumb_instance.getSelector(".window"), {
filter: ".ep",
anchor: "Continuous",
connector: ["StateMachine", {
curviness: 20
}],
connectorStyle: {
strokeStyle: "#5c96bc",
lineWidth: 2,
outlineColor: "transparent",
outlineWidth: 4
}
});
jsplumb_instance.makeTarget(jsplumb_instance.getSelector(".window"), {
dropOptions: {
hoverClass: "dragHover"
},
anchor: "Continuous"
});
}
function updateNodeStyle(gadget, element_id) {
// Update node size according to the zoom level
// XXX does nothing for now
var zoom_level = gadget.props.zoom_level,
element = $(gadget.props.element).find("#" + element_id),
new_value;
$.each(gadget.props.style_attr_list, function(i, j) {
new_value = element.css(j).replace("px", "") * zoom_level + "px";
element.css(j, new_value);
});
}
function removeElement(gadget, node_id) {
var element_id = gadget.props.node_id_to_dom_element_id[node_id];
gadget.props.jsplumb_instance.removeAllEndpoints($(gadget.props.element).find("#" + element_id));
$(gadget.props.element).find("#" + element_id).remove();
delete gadget.props.data.graph.node[node_id];
delete gadget.props.node_id_to_dom_element_id[node_id];
$.each(gadget.props.data.graph.edge, function(k, v) {
if (node_id === v.source || node_id === v.destination) {
delete gadget.props.data.graph.edge[k];
}
});
gadget.notifyDataChanged();
}
function updateElementData(gadget, node_id, data) {
var element_id = gadget.props.node_id_to_dom_element_id[node_id],
new_id = data.id || data.data.id;
$(gadget.props.element).find("#" + element_id).text(data.data.name || new_id)
.attr("title", data.data.name || new_id)
.append('<div class="ep"></div></div>');
delete data.id;
$.extend(gadget.props.data.graph.node[node_id], data.data);
if (new_id && new_id !== node_id) {
gadget.props.data.graph.node[new_id] = gadget.props.data.graph.node[node_id];
delete gadget.props.data.graph.node[node_id];
gadget.props.node_id_to_dom_element_id[new_id] = gadget.props.node_id_to_dom_element_id[node_id];
delete gadget.props.node_id_to_dom_element_id[node_id];
delete gadget.props.data.graph.node[new_id].id;
$.each(gadget.props.data.graph.edge, function (k, v) {
if (v.source === node_id) {
v.source = new_id;
} }
if (v.destination === node_id) { gadget.props.data.graph.node[node_id].coordinate = coordinate;
v.destination = new_id; gadget.notifyDataChanged();
return coordinate;
}
function draggable(gadget) {
var jsplumb_instance = gadget.props.jsplumb_instance;
var stop = function (element) {
updateElementCoordinate(gadget, getNodeId(gadget, element.target.id));
};
// XXX This function should only touch the node element that we just added.
jsplumb_instance.draggable(jsplumb_instance.getSelector(".window"), {
containment: "parent",
grid: [10, 10],
stop: stop
});
jsplumb_instance.makeSource(jsplumb_instance.getSelector(".window"), {
filter: ".ep",
anchor: "Continuous",
connector: ["StateMachine", {
curviness: 20
}],
connectorStyle: {
strokeStyle: "#5c96bc",
lineWidth: 2,
outlineColor: "transparent",
outlineWidth: 4
}
});
jsplumb_instance.makeTarget(jsplumb_instance.getSelector(".window"), {
dropOptions: {
hoverClass: "dragHover"
},
anchor: "Continuous"
});
}
function updateNodeStyle(gadget, element_id) {
// Update node size according to the zoom level
// XXX does nothing for now
var zoom_level = gadget.props.zoom_level;
var element = $(gadget.props.element).find("#" + element_id);
var new_value;
$.each(gadget.props.style_attr_list, function (ignore, j) {
new_value = element.css(j).replace("px", "") * zoom_level + "px";
element.css(j, new_value);
});
}
function removeElement(gadget, node_id) {
var element_id = gadget.props.node_id_to_dom_element_id[node_id];
gadget.props.jsplumb_instance.removeAllEndpoints($(gadget.props.element).find("#" + element_id));
$(gadget.props.element).find("#" + element_id).remove();
delete gadget.props.data.graph.node[node_id];
delete gadget.props.node_id_to_dom_element_id[node_id];
$.each(gadget.props.data.graph.edge, function (k, v) {
if (node_id === v.source || node_id === v.destination) {
delete gadget.props.data.graph.edge[k];
}
});
gadget.notifyDataChanged();
}
function updateElementData(gadget, node_id, data) {
var element_id = gadget.props.node_id_to_dom_element_id[node_id];
var new_id = data.id || data.data.id;
$(gadget.props.element).find("#" + element_id).text(data.data.name || new_id)
.attr("title", data.data.name || new_id)
.append("<div class='ep'></div></div>");
delete data.id;
$.extend(gadget.props.data.graph.node[node_id], data.data);
if (new_id && new_id !== node_id) {
gadget.props.data.graph.node[new_id] = gadget.props.data.graph.node[node_id];
delete gadget.props.data.graph.node[node_id];
gadget.props.node_id_to_dom_element_id[new_id] = gadget.props.node_id_to_dom_element_id[node_id];
delete gadget.props.node_id_to_dom_element_id[node_id];
delete gadget.props.data.graph.node[new_id].id;
$.each(gadget.props.data.graph.edge, function (ignore, v) {
if (v.source === node_id) {
v.source = new_id;
}
if (v.destination === node_id) {
v.destination = new_id;
}
});
} }
}); gadget.notifyDataChanged();
} }
gadget.notifyDataChanged();
}
function addEdge(gadget, edge_id, edge_data) {
var overlays = [];
function addEdge(gadget, edge_id, edge_data) { var connection;
var overlays = [], if (edge_data.name) {
connection; overlays = [
if (edge_data.name) { ["Label", {
overlays = [ cssClass: "l1 component label",
["Label", { label: edge_data.name
cssClass: "l1 component label", }]
label: edge_data.name ];
}] }
]; if (gadget.props.data.graph.node[edge_data.source] === undefined) {
} throw new Error("Error adding edge " + edge_id + " Source " + edge_data.source + " does not exist");
if (gadget.props.data.graph.node[edge_data.source] === undefined) { }
throw new Error("Error adding edge " + edge_id + " Source " + edge_data.source + " does not exist"); if (gadget.props.data.graph.node[edge_data.destination] === undefined) {
} throw new Error("Edge adding edge " + edge_id + " Destination " + edge_data.destination + " does not exist");
if (gadget.props.data.graph.node[edge_data.destination] === undefined) { }
throw new Error("Edge adding edge " + edge_id + " Destination " + edge_data.destination + " does not exist"); // If an edge has this data:
} // { _class: 'Edge',
// If an edge has this data: // source: 'N1',
// { _class: 'Edge', // destination: 'N2',
// source: 'N1', // jsplumb_source_endpoint: 'BottomCenter',
// destination: 'N2', // jsplumb_destination_endpoint: 'LeftMiddle',
// jsplumb_source_endpoint: 'BottomCenter', // jsplumb_connector: 'Flowchart' }
// jsplumb_destination_endpoint: 'LeftMiddle', // Then it is rendered using a flowchart connector. The difficulty is that
// jsplumb_connector: 'Flowchart' } // jsplumb does not let you configure the connector type on the edge, but
// Then it is rendered using a flowchart connector. The difficulty is that // on the source endpoint. One solution seem to create all types of
// jsplumb does not let you configure the connector type on the edge, but // endpoints on nodes.
// on the source endpoint. One solution seem to create all types of if (edge_data.jsplumb_connector === "Flowchart") {
// endpoints on nodes. connection = gadget.props.jsplumb_instance.connect({
if (edge_data.jsplumb_connector === "Flowchart") { uuids: [
connection = gadget.props.jsplumb_instance.connect({ edge_data.source + ".flowChart" + edge_data.jsplumb_source_endpoint,
uuids: [edge_data.source + ".flowChart" + edge_data.jsplumb_source_endpoint, edge_data.destination + ".flowChart" + edge_data.jsplumb_destination_endpoint
edge_data.destination + ".flowChart" + edge_data.jsplumb_destination_endpoint ],
], overlays: overlays
overlays: overlays });
}); } else {
} else { connection = gadget.props.jsplumb_instance.connect({
connection = gadget.props.jsplumb_instance.connect({ source: gadget.props.node_id_to_dom_element_id[edge_data.source],
source: gadget.props.node_id_to_dom_element_id[edge_data.source], target: gadget.props.node_id_to_dom_element_id[edge_data.destination],
target: gadget.props.node_id_to_dom_element_id[edge_data.destination], Connector: ["Bezier", {
Connector: ["Bezier", { curviness: 75
curviness: 75 }],
}], overlays: overlays
overlays: overlays });
}); }
} // set data for 'connection' event that will be called "later"
// set data for 'connection' event that will be called "later" gadget.props.data.graph.edge[edge_id] = edge_data;
gadget.props.data.graph.edge[edge_id] = edge_data; // jsplumb assigned an id, but we are controlling ids ourselves.
// jsplumb assigned an id, but we are controlling ids ourselves. connection.id = edge_id;
connection.id = edge_id; }
}
function expandSchema(class_definition, full_schema) {
function expandSchema(class_definition, full_schema) { // minimal expanding of json schema, supports merging allOf and $ref
// minimal expanding of json schema, supports merging allOf and $ref // references
// references // XXX this should probably be moved to fieldset ( and not handle
// XXX this should probably be moved to fieldset ( and not handle // class_definition here)
// class_definition here) function resolveReference(ref, schema) {
var i;
function resolveReference(ref, schema) { var ref_path = ref.substr(2, ref.length); // 2 here is for #/
// 2 here is for #/ var parts = ref_path.split("/");
var i, ref_path = ref.substr(2, ref.length), for (i = 0; i < parts.length; i += 1) {
parts = ref_path.split("/"); schema = schema[parts[i]];
for (i = 0; i < parts.length; i += 1) { }
schema = schema[parts[i]]; return schema;
} }
return schema;
} function clone(obj) {
return JSON.parse(JSON.stringify(obj));
function clone(obj) { }
return JSON.parse(JSON.stringify(obj));
} var referenced;
var i;
var referenced, var property;
i, var expanded_class_definition = clone(class_definition) || {};
property,
expanded_class_definition = clone(class_definition) || {};
if (!expanded_class_definition.properties) {
expanded_class_definition.properties = {};
if (!expanded_class_definition.properties) { }
expanded_class_definition.properties = {}; // expand direct ref
} if (class_definition.$ref) {
// expand direct ref referenced = expandSchema(resolveReference(class_definition.$ref, full_schema.class_definition), full_schema);
if (class_definition.$ref) { $.extend(expanded_class_definition, referenced);
referenced = expandSchema(resolveReference(class_definition.$ref, full_schema.class_definition), full_schema); delete expanded_class_definition.$ref;
$.extend(expanded_class_definition, referenced); }
delete expanded_class_definition.$ref; // expand ref in properties
} for (property in class_definition.properties) {
// expand ref in properties if (class_definition.properties.hasOwnProperty(property)) {
for (property in class_definition.properties) { if (class_definition.properties[property].$ref) {
if (class_definition.properties.hasOwnProperty(property)) { referenced = expandSchema(resolveReference(class_definition.properties[property].$ref, full_schema.class_definition), full_schema);
if (class_definition.properties[property].$ref) { $.extend(expanded_class_definition.properties[property], referenced);
referenced = expandSchema(resolveReference(class_definition.properties[property].$ref, full_schema.class_definition), full_schema); delete expanded_class_definition.properties[property].$ref;
$.extend(expanded_class_definition.properties[property], referenced); } else {
delete expanded_class_definition.properties[property].$ref; if (class_definition.properties[property].type === "object") {
} else { // no reference, but we expand anyway because we need to recurse in case there is a ref in an object property
if (class_definition.properties[property].type === "object") { referenced = expandSchema(class_definition.properties[property], full_schema);
// no reference, but we expand anyway because we need to recurse in case there is a ref in an object property $.extend(expanded_class_definition.properties[property], referenced);
referenced = expandSchema(class_definition.properties[property], full_schema); }
$.extend(expanded_class_definition.properties[property], referenced); }
} }
} }
} if (class_definition.oneOf) {
} expanded_class_definition.oneOf = [];
if (class_definition.oneOf) { for (i = 0; i < class_definition.oneOf.length; i += 1) {
expanded_class_definition.oneOf = []; expanded_class_definition.oneOf.push(expandSchema(class_definition.oneOf[i], full_schema));
for (i = 0; i < class_definition.oneOf.length; i += 1) { }
expanded_class_definition.oneOf.push(expandSchema(class_definition.oneOf[i], full_schema)); }
} if (class_definition.allOf) {
} for (i = 0; i < class_definition.allOf.length; i += 1) {
if (class_definition.allOf) { referenced = expandSchema(class_definition.allOf[i], full_schema);
for (i = 0; i < class_definition.allOf.length; i += 1) { if (referenced.properties) {
referenced = expandSchema(class_definition.allOf[i], full_schema); $.extend(expanded_class_definition.properties, referenced.properties);
if (referenced.properties) { delete referenced.properties;
$.extend(expanded_class_definition.properties, referenced.properties); }
delete referenced.properties; $.extend(expanded_class_definition, referenced);
} }
$.extend(expanded_class_definition, referenced); if (expanded_class_definition.allOf) {
} delete expanded_class_definition.allOf;
if (expanded_class_definition.allOf) { }
delete expanded_class_definition.allOf; }
} if (expanded_class_definition.$ref) {
} delete expanded_class_definition.$ref;
if (expanded_class_definition.$ref) { }
delete expanded_class_definition.$ref; return clone(expanded_class_definition);
} }
return clone(expanded_class_definition);
} function openEdgeEditionDialog(gadget, connection) {
var edge_id = connection.id;
function openEdgeEditionDialog(gadget, connection) { var edge_data = gadget.props.data.graph.edge[edge_id];
var edge_id = connection.id, var edit_popup = $(gadget.props.element).find("#popup-edit-template");
edge_data = gadget.props.data.graph.edge[edge_id], var schema;
edit_popup = $(gadget.props.element).find("#popup-edit-template"), var fieldset_element;
schema, var delete_promise;
fieldset_element, schema = expandSchema(gadget.props.data.class_definition[edge_data._class], gadget.props.data);
delete_promise; // We do not edit source & destination on edge this way.
schema = expandSchema(gadget.props.data.class_definition[edge_data._class], gadget.props.data); delete schema.properties.source;
// We do not edit source & destination on edge this way. delete schema.properties.destination;
delete schema.properties.source; gadget.props.element.insertAdjacentHTML("beforeend", popup_edit_template);
delete schema.properties.destination; edit_popup = $(gadget.props.element).find("#edit-popup");
gadget.props.element.insertAdjacentHTML("beforeend", popup_edit_template); edit_popup.find(".node_class").text(connection.name || connection._class);
edit_popup = $(gadget.props.element).find("#edit-popup"); fieldset_element = edit_popup.find("fieldset")[0];
edit_popup.find(".node_class").text(connection.name || connection._class); edit_popup.dialog();
fieldset_element = edit_popup.find("fieldset")[0]; edit_popup.show();
edit_popup.dialog();
edit_popup.show(); function save_promise(fieldset_gadget) {
return new RSVP.Queue().push(function () {
function save_promise(fieldset_gadget, edge_id) { return promiseEventListener(edit_popup.find(".graph_editor_validate_button")[0], "click", false);
return RSVP.Queue().push(function() { }).push(function (evt) {
return promiseEventListener(edit_popup.find(".graph_editor_validate_button")[0], "click", false); var data = {
}).push(function(evt) { id: $(evt.target[1]).val(),
var data = { data: {}
id: $(evt.target[1]).val(), };
data: {} return fieldset_gadget.getContent().then(function (r) {
}; $.extend(data.data, gadget.props.data.graph.edge[connection.id]);
return fieldset_gadget.getContent().then(function(r) { $.extend(data.data, r);
$.extend(data.data, gadget.props.data.graph.edge[connection.id]); // to redraw, we remove the edge and add again.
$.extend(data.data, r); // but we want to disable events on connection, since event
// to redraw, we remove the edge and add again. // handling promise are executed asynchronously in undefined order,
// but we want to disable events on connection, since event // we cannot just remove and /then/ add, because the new edge is
// handling promise are executed asynchronously in undefined order, // added before the old is removed.
// we cannot just remove and /then/ add, because the new edge is connection.ignoreEvent = true;
// added before the old is removed. gadget.props.jsplumb_instance.detach(connection);
connection.ignoreEvent = true; addEdge(gadget, r.id, data.data);
gadget.props.jsplumb_instance.detach(connection); });
addEdge(gadget, r.id, data.data); });
}); }
}); delete_promise = new RSVP.Queue().push(function () {
} return promiseEventListener(edit_popup.find(".graph_editor_delete_button")[0], "click", false);
delete_promise = new RSVP.Queue().push(function() { }).push(function () {
return promiseEventListener(edit_popup.find(".graph_editor_delete_button")[0], "click", false); // connectionDetached event will remove the edge from data
}).push(function() { gadget.props.jsplumb_instance.detach(connection);
// connectionDetached event will remove the edge from data });
gadget.props.jsplumb_instance.detach(connection); return gadget.declareGadget("../fieldset/index.html", {
}); element: fieldset_element,
return gadget.declareGadget("../fieldset/index.html", { scope: "fieldset"
element: fieldset_element, }).push(function (fieldset_gadget) {
scope: "fieldset" return RSVP.all([fieldset_gadget, fieldset_gadget.render({
}).push(function(fieldset_gadget) { value: edge_data,
return RSVP.all([fieldset_gadget, fieldset_gadget.render({ property_definition: schema
value: edge_data, }, edge_id)]);
property_definition: schema }).push(function (fieldset_gadget) {
}, edge_id)]); edit_popup.dialog("open");
}).push(function(fieldset_gadget) { return fieldset_gadget[0];
edit_popup.dialog("open"); }).push(function (fieldset_gadget) {
return fieldset_gadget[0]; fieldset_gadget.startService(); // XXX
}).push(function(fieldset_gadget) { return fieldset_gadget;
fieldset_gadget.startService(); // XXX }).push(function (fieldset_gadget) {
return fieldset_gadget; // Expose the dialog handling promise so that we can wait for it in
}).push(function(fieldset_gadget) { // test.
// Expose the dialog handling promise so that we can wait for it in gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, edge_id), delete_promise]);
// test. return gadget.props.dialog_promise;
gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, edge_id), delete_promise]); }).push(function () {
return gadget.props.dialog_promise; edit_popup.dialog("close");
}).push(function() { edit_popup.remove();
edit_popup.dialog("close"); delete gadget.props.dialog_promise;
edit_popup.remove(); });
delete gadget.props.dialog_promise; }
});
} function openNodeEditionDialog(gadget, element) {
var node_id = getNodeId(gadget, element.id);
function openNodeEditionDialog(gadget, element) { var node_data = gadget.props.data.graph.node[node_id];
var node_id = getNodeId(gadget, element.id), var node_edit_popup = $(gadget.props.element).find("#popup-edit-template");
node_data = gadget.props.data.graph.node[node_id], var schema;
node_edit_popup = $(gadget.props.element).find("#popup-edit-template"), var fieldset_element;
schema, var delete_promise;
fieldset_element, // If we have no definition for this, we do not allow edition.
delete_promise; // XXX incorrect, we need to display this dialog to be able
// If we have no definition for this, we do not allow edition. // to delete a node
// XXX incorrect, we need to display this dialog to be able if (gadget.props.data.class_definition[node_data._class] === undefined) {
// to delete a node return false;
if (gadget.props.data.class_definition[node_data._class] === undefined) { }
return false; schema = expandSchema(gadget.props.data.class_definition[node_data._class], gadget.props.data);
} if (node_edit_popup.length !== 0) {
schema = expandSchema(gadget.props.data.class_definition[node_data._class], gadget.props.data); node_edit_popup.remove();
if (node_edit_popup.length !== 0) { }
node_edit_popup.remove(); gadget.props.element.insertAdjacentHTML("beforeend", popup_edit_template);
} node_edit_popup = $(gadget.props.element).find("#edit-popup");
gadget.props.element.insertAdjacentHTML("beforeend", popup_edit_template); // Set the name of the popup to the node class
node_edit_popup = $(gadget.props.element).find("#edit-popup"); node_edit_popup.find(".node_class").text(node_data.name || node_data._class);
// Set the name of the popup to the node class fieldset_element = node_edit_popup.find("fieldset")[0];
node_edit_popup.find(".node_class").text(node_data.name || node_data._class); node_edit_popup.dialog();
fieldset_element = node_edit_popup.find("fieldset")[0]; node_data.id = node_id;
node_edit_popup.dialog();
node_data.id = node_id; function save_promise(fieldset_gadget, node_id) {
return new RSVP.Queue().push(function () {
function save_promise(fieldset_gadget, node_id) { return promiseEventListener(
return RSVP.Queue().push(function() { node_edit_popup.find(".graph_editor_validate_button")[0],
return promiseEventListener(node_edit_popup.find(".graph_editor_validate_button")[0], "click", false); "click",
}).push(function(evt) { false
var data = { );
// XXX id should not be handled differently ... }).push(function (evt) {
id: $(evt.target[1]).val(), var data = {
data: {} // XXX id should not be handled differently ...
}; id: $(evt.target[1]).val(),
return fieldset_gadget.getContent().then(function(r) { data: {}
$.extend(data.data, r); };
updateElementData(gadget, node_id, data); return fieldset_gadget.getContent().then(function (r) {
}); $.extend(data.data, r);
}); updateElementData(gadget, node_id, data);
} });
delete_promise = new RSVP.Queue().push(function() { });
return promiseEventListener(node_edit_popup.find(".graph_editor_delete_button")[0], "click", false); }
}).push(function() { delete_promise = new RSVP.Queue().push(function () {
return removeElement(gadget, node_id); return promiseEventListener(
}); node_edit_popup.find(".graph_editor_delete_button")[0],
return gadget.declareGadget("../fieldset/index.html", { "click",
element: fieldset_element, false
scope: "fieldset" );
}).push(function(fieldset_gadget) { }).push(function () {
return RSVP.all([fieldset_gadget, fieldset_gadget.render({ return removeElement(gadget, node_id);
value: node_data, });
property_definition: schema return gadget.declareGadget("../fieldset/index.html", {
}, node_id)]); element: fieldset_element,
}).push(function(fieldset_gadget) { scope: "fieldset"
node_edit_popup.dialog("open"); }).push(function (fieldset_gadget) {
return fieldset_gadget[0]; return RSVP.all([
}).push(function(fieldset_gadget) { fieldset_gadget,
fieldset_gadget.startService(); // XXX this should not be needed anymore. fieldset_gadget.render(
return fieldset_gadget; {
}).push(function(fieldset_gadget) { value: node_data,
// Expose the dialog handling promise so that we can wait for it in property_definition: schema
// test. },
gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, node_id), delete_promise]); node_id
return gadget.props.dialog_promise; )
}).push(function() { ]);
node_edit_popup.dialog("close"); }).push(function (fieldset_gadget) {
node_edit_popup.remove(); node_edit_popup.dialog("open");
delete gadget.props.dialog_promise; return fieldset_gadget[0];
}); }).push(function (fieldset_gadget) {
} fieldset_gadget.startService(); // XXX this should not be needed anymore.
return fieldset_gadget;
function waitForConnection(gadget) { }).push(function (fieldset_gadget) {
return loopJsplumbBind(gadget, "connection", function(info, originalEvent) { // Expose the dialog handling promise so that we can wait for it in
updateConnectionData(gadget, info.connection, false); // test.
}); gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, node_id), delete_promise]);
} return gadget.props.dialog_promise;
}).push(function () {
function waitForConnectionDetached(gadget) { node_edit_popup.dialog("close");
return loopJsplumbBind(gadget, "connectionDetached", function(info, originalEvent) { node_edit_popup.remove();
updateConnectionData(gadget, info.connection, true); delete gadget.props.dialog_promise;
}); });
} }
function waitForConnectionClick(gadget) { function waitForConnection(gadget) {
return loopJsplumbBind(gadget, "click", function(connection) { return loopJsplumbBind(gadget, "connection", function (info) {
return openEdgeEditionDialog(gadget, connection); updateConnectionData(gadget, info.connection, false);
}); });
} }
function addNode(gadget, node_id, node_data) { function waitForConnectionDetached(gadget) {
var render_element = $(gadget.props.main), return loopJsplumbBind(gadget, "connectionDetached", function (info) {
class_definition = gadget.props.data.class_definition[node_data._class], updateConnectionData(gadget, info.connection, true);
coordinate = node_data.coordinate, });
dom_element_id, }
box,
absolute_position, function waitForConnectionClick(gadget) {
domElement; return loopJsplumbBind(gadget, "click", function (connection) {
return openEdgeEditionDialog(gadget, connection);
dom_element_id = generateDomElementId(gadget.props.element); });
gadget.props.node_id_to_dom_element_id[node_id] = dom_element_id; }
node_data.name = node_data.name || class_definition.name;
gadget.props.data.graph.node[node_id] = node_data; function addNode(gadget, node_id, node_data) {
if (coordinate === undefined) { var render_element = $(gadget.props.main);
coordinate = { var class_definition = gadget.props.data.class_definition[node_data._class];
top: 0, var coordinate = node_data.coordinate;
left: 0 var dom_element_id;
}; var box;
} var absolute_position;
node_data.coordinate = updateElementCoordinate(gadget, node_id, coordinate); var domElement;
/*jslint nomen: true*/
domElement = domParser.parseFromString(node_template({ dom_element_id = generateDomElementId(gadget.props.element);
"class": node_data._class.replace(".", "-"), gadget.props.node_id_to_dom_element_id[node_id] = dom_element_id;
element_id: dom_element_id, node_data.name = node_data.name || class_definition.name;
title: node_data.name || node_data.id, gadget.props.data.graph.node[node_id] = node_data;
name: node_data.name || node_data.id if (coordinate === undefined) {
}), "text/html").querySelector(".window"); coordinate = {
render_element.append(domElement); top: 0,
box = $(gadget.props.element).find("#" + dom_element_id); left: 0
absolute_position = convertToAbsolutePosition(gadget, coordinate.left, coordinate.top); };
if (class_definition && class_definition.css) { }
box.css(class_definition.css); node_data.coordinate = updateElementCoordinate(gadget, node_id, coordinate);
} domElement = domParser.parseFromString(node_template({
box.css("top", absolute_position[1]); "class": node_data._class.replace(".", "-"),
box.css("left", absolute_position[0]); element_id: dom_element_id,
updateNodeStyle(gadget, dom_element_id); title: node_data.name || node_data.id,
draggable(gadget); name: node_data.name || node_data.id
// XXX make only this element draggable. }), "text/html").querySelector(".window");
// Add some flowchart endpoints render_element.append(domElement);
// TODO: add them all ! box = $(gadget.props.element).find("#" + dom_element_id);
gadget.props.jsplumb_instance.addEndpoint(dom_element_id, { absolute_position = convertToAbsolutePosition(gadget, coordinate.left, coordinate.top);
isSource: true, if (class_definition && class_definition.css) {
maxConnections: -1, box.css(class_definition.css);
connector: ["Flowchart", { }
stub: [40, 60], box.css("top", absolute_position[1]);
gap: 10, box.css("left", absolute_position[0]);
cornerRadius: 5, updateNodeStyle(gadget, dom_element_id);
alwaysRespectStubs: true draggable(gadget);
}] // XXX make only this element draggable.
}, { // Add some flowchart endpoints
anchor: "BottomCenter", // TODO: add them all !
uuid: node_id + ".flowchartBottomCenter" gadget.props.jsplumb_instance.addEndpoint(dom_element_id, {
}); isSource: true,
gadget.props.jsplumb_instance.addEndpoint(dom_element_id, { maxConnections: -1,
isTarget: true, connector: ["Flowchart", {
maxConnections: -1 stub: [40, 60],
}, { gap: 10,
anchor: "LeftMiddle", cornerRadius: 5,
uuid: node_id + ".flowChartLeftMiddle" alwaysRespectStubs: true
}); }]
gadget.notifyDataChanged(); }, {
} anchor: "BottomCenter",
uuid: node_id + ".flowchartBottomCenter"
function waitForDrop(gadget) { });
var callback; gadget.props.jsplumb_instance.addEndpoint(dom_element_id, {
isTarget: true,
function canceller() { maxConnections: -1
if (callback !== undefined) { }, {
gadget.props.main.removeEventListener("drop", callback, false); anchor: "LeftMiddle",
} uuid: node_id + ".flowChartLeftMiddle"
} });
/*jslint unparam: true*/ gadget.notifyDataChanged();
function resolver(resolve, reject) { }
callback = function(evt) {
try { function waitForDrop(gadget) {
var class_name, offset = $(gadget.props.main).offset(), var callback;
relative_position = convertToRelativePosition(gadget, evt.clientX - offset.left + "px", evt.clientY - offset.top + "px");
try { function canceller() {
// html5 compliant browser if (callback !== undefined) {
class_name = JSON.parse(evt.dataTransfer.getData("application/json")); gadget.props.main.removeEventListener("drop", callback, false);
} catch (e) { }
// internet explorer }
class_name = JSON.parse(evt.dataTransfer.getData("text")); function resolver(ignore, reject) {
callback = function (evt) {
try {
var class_name;
var offset = $(gadget.props.main).offset();
var relative_position = convertToRelativePosition(gadget, evt.clientX - offset.left + "px", evt.clientY - offset.top + "px");
try {
// html5 compliant browser
class_name = JSON.parse(evt.dataTransfer.getData("application/json"));
} catch (error_from_drop) {
// internet explorer
class_name = JSON.parse(evt.dataTransfer.getData("text"));
}
addNode(gadget, generateNodeId(gadget, {
_class: class_name
}), {
coordinate: {
left: relative_position[0],
top: relative_position[1]
},
_class: class_name
});
} catch (e) {
reject(e);
}
};
gadget.props.main.addEventListener("drop", callback, false);
}
return RSVP.all([ // loopEventListener adds an event listener that will prevent default for
// dragover
loopEventListener(gadget.props.main, "dragover", false, function () {
return undefined;
}), new RSVP.Promise(resolver, canceller)
]);
}
gadget_klass.ready(function (g) {
g.props = {};
}).ready(function (g) {
return g.getElement().push(function (element) {
g.props.element = element;
});
}).ready(function (g) {
g.props.node_id_to_dom_element_id = {};
g.props.zoom_level = 1;
g.props.style_attr_list = ["width", "height", "padding-top", "line-height"];
g.getElement().then(function (element) {
g.props.element = element;
});
}).declareAcquiredMethod("notifyDataChanged", "notifyDataChanged")
.declareMethod("render", function (data) {
var gadget = this;
this.props.data = {};
if (data.key) {
// Gadget embedded in ERP5
this.props.erp5_key = data.key;
data = data.value;
}
this.props.main = this.props.element.querySelector(".graph_container");
/*
$(this.props.main).resizable({
resize : function (event, ui) {
jsplumb_instance.repaint(ui.helper);
} }
addNode(gadget, generateNodeId(gadget, {
_class: class_name
}), {
coordinate: {
left: relative_position[0],
top: relative_position[1]
},
_class: class_name
});
} catch (e) {
reject(e);
}
};
gadget.props.main.addEventListener("drop", callback, false);
}
return new RSVP.all([ // loopEventListener adds an event listener that will prevent default for
// dragover
loopEventListener(gadget.props.main, "dragover", false, function() {
return undefined;
}), RSVP.Promise(resolver, canceller)
]);
}
gadget_klass.ready(function (g) {
g.props = {};
})
.ready(function (g) {
return g.getElement().push(function (element) {
g.props.element = element;
});
})
.ready(function(g) {
g.props.node_id_to_dom_element_id = {};
g.props.zoom_level = 1;
g.props.style_attr_list = ["width", "height", "padding-top", "line-height"];
g.getElement().then(function(element) {
g.props.element = element;
});
})
.declareAcquiredMethod("notifyDataChanged", "notifyDataChanged")
.declareMethod("render", function(data) {
var gadget = this, jsplumb_instance;
this.props.data = {};
if (data.key) {
// Gadget embedded in ERP5
this.props.erp5_key = data.key;
data = data.value;
}
this.props.main = this.props.element.querySelector(".graph_container");
/*
$(this.props.main).resizable({
resize : function(event, ui) {
jsplumb_instance.repaint(ui.helper);
}
});
*/
if (data) {
this.props.data = JSON.parse(data);
// XXX how to make queue ??
return layoutGraph(this.props.data.graph).then(function(graph_data) {
gadget.props.data.graph = graph_data;
// load the data
$.each(gadget.props.data.graph.node, function(key, value) {
addNode(gadget, key, value);
});
$.each(gadget.props.data.graph.edge, function(key, value) {
addEdge(gadget, key, value);
}); });
}); */
} if (data) {
}) this.props.data = JSON.parse(data);
.declareMethod("getContent", function() {
var ret = {}; // XXX how to make queue ??
if (this.props.erp5_key) { return layoutGraph(this.props.data.graph).then(function (graph_data) {
// ERP5 gadget.props.data.graph = graph_data;
ret[this.props.erp5_key] = JSON.stringify(this.props.data); // load the data
return ret; $.each(gadget.props.data.graph.node, function (key, value) {
} addNode(gadget, key, value);
return JSON.stringify(this.props.data); });
}) $.each(gadget.props.data.graph.edge, function (key, value) {
.onEvent("dblclick", function (evt) { addEdge(gadget, key, value);
var node = evt.target; });
if ((node.nodeType === Node.ELEMENT_NODE) && });
(node.tagName === "DIV") && node.classList.contains(['window'])) { }
return openNodeEditionDialog(this, node); })
} .declareMethod("getContent", function () {
}) var ret = {};
.declareService(function() { if (this.props.erp5_key) {
var gadget = this, jsplumb_instance; // ERP5
this.props.main = this.props.element.querySelector(".graph_container"); ret[this.props.erp5_key] = JSON.stringify(this.props.data);
this.props.jsplumb_instance = jsplumb_instance = jsPlumb.getInstance(); return ret;
if (this.props.data) { }
// load the data return JSON.stringify(this.props.data);
$.each(this.props.data.graph.node, function(key, value) { })
addNode(gadget, key, value); .onEvent("dblclick", function (evt) {
}); var node = evt.target;
$.each(this.props.data.graph.edge, function(key, value) { if (
addEdge(gadget, key, value); (node.nodeType === Node.ELEMENT_NODE) &&
}); (node.tagName === "DIV") && node.classList.contains(["window"])
} ) {
jsplumb_instance.setRenderMode(jsplumb_instance.SVG); return openNodeEditionDialog(this, node);
jsplumb_instance.importDefaults({ }
HoverPaintStyle: { })
strokeStyle: "#1e8151", .declareService(function () {
lineWidth: 2 var gadget = this;
}, var jsplumb_instance;
Endpoint: ["Dot", { this.props.main = this.props.element.querySelector(".graph_container");
radius: 2 this.props.jsplumb_instance = jsplumb_instance = jsPlumb.getInstance();
}], if (this.props.data) {
ConnectionOverlays: [ // load the data
["Arrow", { $.each(this.props.data.graph.node, function (key, value) {
location: 1, addNode(gadget, key, value);
id: "arrow", });
length: 14, $.each(this.props.data.graph.edge, function (key, value) {
foldback: 0.8 addEdge(gadget, key, value);
}] });
], }
Container: this.props.main jsplumb_instance.setRenderMode(jsplumb_instance.SVG);
}); jsplumb_instance.importDefaults({
draggable(gadget); HoverPaintStyle: {
strokeStyle: "#1e8151",
return RSVP.all([waitForDrop(gadget), lineWidth: 2
waitForConnection(gadget), },
waitForConnectionDetached(gadget), Endpoint: ["Dot", {
waitForConnectionClick(gadget) radius: 2
]); }],
}); ConnectionOverlays: [
["Arrow", {
})(RSVP, rJS, $, jsPlumb, Handlebars, loopEventListener, promiseEventListener, DOMParser, Springy); location: 1,
\ No newline at end of file id: "arrow",
length: 14,
foldback: 0.8
}]
],
Container: this.props.main
});
draggable(gadget);
return RSVP.all([
waitForDrop(gadget),
waitForConnection(gadget),
waitForConnectionDetached(gadget),
waitForConnectionClick(gadget)
]);
});
}(RSVP, rJS, $, jsPlumb, Handlebars, loopEventListener, promiseEventListener, DOMParser, Springy));
\ No newline at end of file
/*global window, document, rJS, JSON, QUnit, jQuery, RSVP, console, setTimeout /*jslint vars:true nomen:true */ /* these two options are for compatibility with jslint 2014-04-21 . We'll remove them once we switch to more recent jslint */
/*global window, document, rJS, JSON, QUnit, jQuery, RSVP, console, setTimeout */
*/ (function (rJS, JSON, QUnit, RSVP, $) {
(function(rJS, JSON, QUnit, RSVP, $) {
"use strict"; "use strict";
var start = QUnit.start, var start = QUnit.start;
stop = QUnit.stop, var stop = QUnit.stop;
test = QUnit.test, var test = QUnit.test;
equal = QUnit.equal, var equal = QUnit.equal;
ok = QUnit.ok, var ok = QUnit.ok;
error_handler = function(e) { var error_handler = function (e) {
window.console.error(e); window.console.error(e);
ok(false, e); ok(false, e);
}, };
sample_class_definition = { var sample_class_definition = {
edge: { edge: {
description: "Base definition for edge", description: "Base definition for edge",
properties: { properties: {
_class: { "_class": {
type: "string" type: "string"
}, },
destination: { destination: {
...@@ -25,7 +24,7 @@ ...@@ -25,7 +24,7 @@
name: { name: {
type: "string" type: "string"
}, },
required: [ "name", "_class", "source", "destination" ], required: ["name", "_class", "source", "destination"],
source: { source: {
type: "string" type: "string"
} }
...@@ -33,35 +32,35 @@ ...@@ -33,35 +32,35 @@
type: "object" type: "object"
}, },
"Example.Edge": { "Example.Edge": {
_class: "edge", "_class": "edge",
allOf: [ { allOf: [{
$ref: "#/edge" "$ref": "#/edge"
}, { }, {
properties: { properties: {
color: { color: {
"enum": [ "red", "green", "blue" ] "enum": ["red", "green", "blue"]
} }
} }
} ], }],
description: "An example edge with a color property" description: "An example edge with a color property"
}, },
"Example.Node": { "Example.Node": {
_class: "node", "_class": "node",
allOf: [ { allOf: [{
$ref: "#/node" "$ref": "#/node"
}, { }, {
properties: { properties: {
shape: { shape: {
type: "string" type: "string"
} }
} }
} ], }],
description: "An example node with a shape property" description: "An example node with a shape property"
}, },
node: { node: {
description: "Base definition for node", description: "Base definition for node",
properties: { properties: {
_class: { "_class": {
type: "string" type: "string"
}, },
coordinate: { coordinate: {
...@@ -74,14 +73,15 @@ ...@@ -74,14 +73,15 @@
name: { name: {
type: "string" type: "string"
}, },
required: [ "name", "_class" ] required: ["name", "_class"]
}, },
type: "object" type: "object"
} }
}, sample_graph = { };
var sample_graph = {
edge: { edge: {
edge1: { edge1: {
_class: "Example.Edge", "_class": "Example.Edge",
source: "N1", source: "N1",
destination: "N2", destination: "N2",
color: "blue" color: "blue"
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
}, },
node: { node: {
N1: { N1: {
_class: "Example.Node", "_class": "Example.Node",
name: "Node 1", name: "Node 1",
coordinate: { coordinate: {
top: 0, top: 0,
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
shape: "square" shape: "square"
}, },
N2: { N2: {
_class: "Example.Node", "_class": "Example.Node",
name: "Node 2", name: "Node 2",
shape: "circle", shape: "circle",
coordinate: { coordinate: {
...@@ -107,10 +107,11 @@ ...@@ -107,10 +107,11 @@
} }
} }
} }
}, sample_graph_no_node_coodinate = { };
var sample_graph_no_node_coodinate = {
edge: { edge: {
edge1: { edge1: {
_class: "Example.Edge", "_class": "Example.Edge",
source: "N1", source: "N1",
destination: "N2", destination: "N2",
color: "blue" color: "blue"
...@@ -118,110 +119,118 @@ ...@@ -118,110 +119,118 @@
}, },
node: { node: {
N1: { N1: {
_class: "Example.Node", "_class": "Example.Node",
name: "Node 1", name: "Node 1",
shape: "square" shape: "square"
}, },
N2: { N2: {
_class: "Example.Node", "_class": "Example.Node",
name: "Node 2", name: "Node 2",
shape: "circle" shape: "circle"
} }
} }
}, sample_graph_not_connected = { };
var sample_graph_not_connected = {
edge: {}, edge: {},
node: { node: {
N1: { N1: {
_class: "Example.Node", "_class": "Example.Node",
name: "Node 1", name: "Node 1",
shape: "square" shape: "square"
}, },
N2: { N2: {
_class: "Example.Node", "_class": "Example.Node",
name: "Node 2", name: "Node 2",
shape: "circle" shape: "circle"
} }
} }
}, sample_data_graph = JSON.stringify({ };
var sample_data_graph = JSON.stringify({
class_definition: sample_class_definition, class_definition: sample_class_definition,
graph: sample_graph graph: sample_graph
}), sample_data_graph_no_node_coordinate = JSON.stringify({ });
var sample_data_graph_no_node_coordinate = JSON.stringify({
class_definition: sample_class_definition, class_definition: sample_class_definition,
graph: sample_graph_no_node_coodinate graph: sample_graph_no_node_coodinate
}), sample_data_graph_not_connected = JSON.stringify({ });
var sample_data_graph_not_connected = JSON.stringify({
class_definition: sample_class_definition, class_definition: sample_class_definition,
graph: sample_graph_not_connected graph: sample_graph_not_connected
}), sample_data_empty_graph = JSON.stringify({ });
var sample_data_empty_graph = JSON.stringify({
class_definition: sample_class_definition, class_definition: sample_class_definition,
graph: { graph: {
node: {}, node: {},
edge: {} edge: {}
} }
}); });
QUnit.config.testTimeout = 60000; QUnit.config.testTimeout = 60000;
rJS(window).ready(function(g) { rJS(window).ready(function (g) {
test("Sample graph can be loaded and output is equal to input", function() { test("Sample graph can be loaded and output is equal to input", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#test-element") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
return jsplumb_gadget.render(sample_data_graph); return jsplumb_gadget.render(sample_data_graph);
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
equal(content, sample_data_graph); equal(content, sample_data_graph);
}).fail(error_handler).always(start); }).fail(error_handler).always(start);
}); });
test("New node can be drag & dropped", function() { test("New node can be drag & dropped", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
// XXX here I used getContent to have a promise, but there must be a // XXX here I used getContent to have a promise, but there must be a
// more elegant way. // more elegant way.
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// fake a drop event // fake a drop event
var e = new window.Event("drop"); var e = new window.Event("drop");
e.dataTransfer = { e.dataTransfer = {
getData: function(type) { getData: function (type) {
// make sure we are called properly // make sure we are called properly
equal("application/json", type, "The drag&dropped element must have data type application/json"); equal("application/json", type, "The drag&dropped element must have data type application/json");
return JSON.stringify("Example.Node"); return JSON.stringify("Example.Node");
} }
}; };
jsplumb_gadget.props.main.dispatchEvent(e); jsplumb_gadget.props.main.dispatchEvent(e);
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
var node, graph = JSON.parse(content).graph; var node;
var graph = JSON.parse(content).graph;
equal(1, Object.keys(graph.node).length, "There is one new node class"); equal(1, Object.keys(graph.node).length, "There is one new node class");
node = graph.node[Object.keys(graph.node)[0]]; node = graph.node[Object.keys(graph.node)[0]];
equal("Example.Node", node._class, "Node class is set to Example.?ode"); equal("Example.Node", node._class, "Node class is set to Example.Node");
}); });
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#test-element") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_empty_graph); jsplumb_gadget.render(sample_data_empty_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Node can be dragged", function() { test("Node can be dragged", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// 100 and 60 are about 10% of the .graph_container div ( set by css, so this // 100 and 60 are about 10% of the .graph_container div ( set by css, so this
// might change ) // might change )
$("div[title='Node 1']").simulate("drag", { $("div[title='Node 1']").simulate("drag", {
dx: 100, dx: 100,
dy: 60 dy: 60
}); });
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
var graph = JSON.parse(content).graph, node_coordinate = graph.node.N1.coordinate; var graph = JSON.parse(content).graph;
var node_coordinate = graph.node.N1.coordinate;
// Since original coordinates where 0,0 we are now about 0.1,0.1 // Since original coordinates where 0,0 we are now about 0.1,0.1
// as we moved 10% // as we moved 10%
ok(node_coordinate.top - 0.1 < 0.1, "Top is ok"); ok(node_coordinate.top - 0.1 < 0.1, "Top is ok");
...@@ -230,22 +239,22 @@ ...@@ -230,22 +239,22 @@
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#test-element") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph); jsplumb_gadget.render(sample_data_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Node properties can be edited", function() { test("Node properties can be edited", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// click on a node to see display the popup // click on a node to see display the popup
$("div[title='Node 1']").simulate("dblclick"); $("div[title='Node 1']").simulate("dblclick");
// Promises that handle the dialog actions are not available // Promises that handle the dialog actions are not available
// immediately after clicking. // immediately after clicking.
var promise = RSVP.Promise(function(resolve) { var promise = new RSVP.Promise(function (resolve) {
var fillDialog = function() { function fillDialog() {
if (!jsplumb_gadget.props.dialog_promise) { if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later. // Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait // XXX this condition is actually incorrect. We need to wait
...@@ -266,12 +275,13 @@ ...@@ -266,12 +275,13 @@
// resolve our test promise once the dialog handling promise is // resolve our test promise once the dialog handling promise is
// finished. // finished.
jsplumb_gadget.props.dialog_promise.then(resolve); jsplumb_gadget.props.dialog_promise.then(resolve);
}; }
fillDialog(); fillDialog();
}); });
return promise.then(function() { return promise.then(function () {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph, node = graph.node.N1; var graph = JSON.parse(content).graph;
var node = graph.node.N1;
equal("Modified Name", node.name, "Data is modified"); equal("Modified Name", node.name, "Data is modified");
equal("Modified Name", $("div#" + jsplumb_gadget.props.node_id_to_dom_element_id.N1).text(), "DOM is modified"); equal("Modified Name", $("div#" + jsplumb_gadget.props.node_id_to_dom_element_id.N1).text(), "DOM is modified");
equal(1, $("div[title='Modified Name']").length, "DOM title attribute is modified"); equal(1, $("div[title='Modified Name']").length, "DOM title attribute is modified");
...@@ -281,27 +291,28 @@ ...@@ -281,27 +291,28 @@
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#test-element") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph); jsplumb_gadget.render(sample_data_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Node can be connected", function() { test("Node can be connected", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var node1 = jsplumb_gadget.props.main.querySelector("div[title='Node 1']"), var node1 = jsplumb_gadget.props.main.querySelector("div[title='Node 1']");
node2 = jsplumb_gadget.props.main.querySelector("div[title='Node 2']"); var node2 = jsplumb_gadget.props.main.querySelector("div[title='Node 2']");
equal(0, Object.keys(JSON.parse(content).graph.edge).length, "There are no edge at the beginning"); equal(0, Object.keys(JSON.parse(content).graph.edge).length, "There are no edge at the beginning");
jsplumb_gadget.props.jsplumb_instance.connect({ jsplumb_gadget.props.jsplumb_instance.connect({
source: node1.id, source: node1.id,
target: node2.id target: node2.id
}); });
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
var edge, graph = JSON.parse(content).graph; var edge;
var graph = JSON.parse(content).graph;
equal(2, Object.keys(graph.node).length, "We still have 2 nodes"); equal(2, Object.keys(graph.node).length, "We still have 2 nodes");
equal(1, Object.keys(graph.edge).length, "We have 1 edge"); equal(1, Object.keys(graph.edge).length, "We have 1 edge");
edge = graph.edge[Object.keys(graph.edge)[0]]; edge = graph.edge[Object.keys(graph.edge)[0]];
...@@ -313,24 +324,24 @@ ...@@ -313,24 +324,24 @@
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#test-element") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph_not_connected); jsplumb_gadget.render(sample_data_graph_not_connected);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Node can be deleted", function() { test("Node can be deleted", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
equal(1, $("div[title='Node 1']").length, "node 1 is visible"); equal(1, $("div[title='Node 1']").length, "node 1 is visible");
equal(1, $("._jsPlumb_connector").length, "there is 1 connection"); equal(1, $("._jsPlumb_connector").length, "there is 1 connection");
// click on node 1 to see display the popup // click on node 1 to see display the popup
$("div[title='Node 1']").simulate("dblclick"); $("div[title='Node 1']").simulate("dblclick");
// Promises that handle the dialog actions are not available // Promises that handle the dialog actions are not available
// immediately after clicking. // immediately after clicking.
var promise = RSVP.Promise(function(resolve) { var promise = new RSVP.Promise(function (resolve) {
var waitForDialogAndDelete = function() { function waitForDialogAndDelete() {
if (!jsplumb_gadget.props.dialog_promise) { if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later. // Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait // XXX this condition is actually incorrect. We need to wait
...@@ -344,11 +355,11 @@ ...@@ -344,11 +355,11 @@
// resolve our test promise once the dialog handling promise is // resolve our test promise once the dialog handling promise is
// finished. // finished.
jsplumb_gadget.props.dialog_promise.then(resolve); jsplumb_gadget.props.dialog_promise.then(resolve);
}; }
waitForDialogAndDelete(); waitForDialogAndDelete();
}); });
return promise.then(function() { return promise.then(function () {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph; var graph = JSON.parse(content).graph;
equal(1, Object.keys(graph.node).length, "node is removed from data"); equal(1, Object.keys(graph.node).length, "node is removed from data");
equal(0, Object.keys(graph.edge).length, "edge referencing this node is also removed"); equal(0, Object.keys(graph.edge).length, "edge referencing this node is also removed");
...@@ -360,22 +371,22 @@ ...@@ -360,22 +371,22 @@
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#test-element") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph); jsplumb_gadget.render(sample_data_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Node id can be changed (connections are updated and node can be edited afterwards)", function() { test("Node id can be changed (connections are updated and node can be edited afterwards)", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// click on a node to see display the popup // click on a node to see display the popup
$("div[title='Node 1']").simulate("dblclick"); $("div[title='Node 1']").simulate("dblclick");
// Promises that handle the dialog actions are not available // Promises that handle the dialog actions are not available
// immediately after clicking. // immediately after clicking.
var promise = RSVP.Promise(function(resolve) { var promise = new RSVP.Promise(function (resolve) {
var fillDialog = function() { function fillDialog() {
if (!jsplumb_gadget.props.dialog_promise) { if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later. // Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait // XXX this condition is actually incorrect. We need to wait
...@@ -392,11 +403,11 @@ ...@@ -392,11 +403,11 @@
// resolve our test promise once the dialog handling promise is // resolve our test promise once the dialog handling promise is
// finished. // finished.
jsplumb_gadget.props.dialog_promise.then(resolve); jsplumb_gadget.props.dialog_promise.then(resolve);
}; }
fillDialog(); fillDialog();
}); });
return promise.then(function() { return promise.then(function () {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph; var graph = JSON.parse(content).graph;
equal(2, Object.keys(graph.node).length, "We still have two nodes"); equal(2, Object.keys(graph.node).length, "We still have two nodes");
ok(graph.node.N1b !== undefined, "Node Id changed"); ok(graph.node.N1b !== undefined, "Node Id changed");
...@@ -408,45 +419,47 @@ ...@@ -408,45 +419,47 @@
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#test-element") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph); jsplumb_gadget.render(sample_data_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("New node can be edited", function() { test("New node can be edited", function () {
var jsplumb_gadget, node_id; var jsplumb_gadget;
var node_id;
stop(); stop();
function runTest() { function runTest() {
// XXX here I used getContent to have a promise, but there must be a // XXX here I used getContent to have a promise, but there must be a
// more elegant way. // more elegant way.
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// fake a drop event // fake a drop event
var e = new window.Event("drop"); var e = new window.Event("drop");
e.dataTransfer = { e.dataTransfer = {
getData: function(type) { getData: function (type) {
// make sure we are called properly // make sure we are called properly
equal("application/json", type, "The drag&dropped element must have data type application/json"); equal("application/json", type, "The drag&dropped element must have data type application/json");
return JSON.stringify("Example.Node"); return JSON.stringify("Example.Node");
} }
}; };
jsplumb_gadget.props.main.dispatchEvent(e); jsplumb_gadget.props.main.dispatchEvent(e);
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
var node, graph = JSON.parse(content).graph; var node;
var graph = JSON.parse(content).graph;
equal(1, Object.keys(graph.node).length); equal(1, Object.keys(graph.node).length);
node_id = Object.keys(graph.node)[0]; node_id = Object.keys(graph.node)[0];
node = graph.node[node_id]; node = graph.node[node_id];
equal("Example.Node", node._class); equal("Example.Node", node._class);
}).then(function() { }).then(function () {
// click the new node to see display the popup // click the new node to see display the popup
// XXX at the moment nodes have class window // XXX at the moment nodes have class window
equal(1, $("div.window").length, "We have a new node"); equal(1, $("div.window").length, "We have a new node");
$("div.window").simulate("dblclick"); $("div.window").simulate("dblclick");
// Promises that handle the dialog actions are not available // Promises that handle the dialog actions are not available
// immediately after clicking. // immediately after clicking.
var promise = RSVP.Promise(function(resolve) { var promise = new RSVP.Promise(function (resolve) {
var fillDialog = function() { function fillDialog() {
if (!jsplumb_gadget.props.dialog_promise) { if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later. // Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait // XXX this condition is actually incorrect. We need to wait
...@@ -467,12 +480,13 @@ ...@@ -467,12 +480,13 @@
// resolve our test promise once the dialog handling promise is // resolve our test promise once the dialog handling promise is
// finished. // finished.
jsplumb_gadget.props.dialog_promise.then(resolve); jsplumb_gadget.props.dialog_promise.then(resolve);
}; }
fillDialog(); fillDialog();
}); });
return promise.then(function() { return promise.then(function () {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph, node = graph.node[node_id]; var graph = JSON.parse(content).graph;
var node = graph.node[node_id];
equal("Modified Name", node.name, "Data is modified"); equal("Modified Name", node.name, "Data is modified");
equal("Modified Name", $("div.window").text(), "DOM is modified"); equal("Modified Name", $("div.window").text(), "DOM is modified");
}); });
...@@ -481,45 +495,47 @@ ...@@ -481,45 +495,47 @@
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#test-element") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_empty_graph); jsplumb_gadget.render(sample_data_empty_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("New node can be deleted", function() { test("New node can be deleted", function () {
var jsplumb_gadget, node_id; var jsplumb_gadget;
var node_id;
stop(); stop();
function runTest() { function runTest() {
// XXX here I used getContent to have a promise, but there must be a // XXX here I used getContent to have a promise, but there must be a
// more elegant way. // more elegant way.
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// fake a drop event // fake a drop event
var e = new window.Event("drop"); var e = new window.Event("drop");
e.dataTransfer = { e.dataTransfer = {
getData: function(type) { getData: function (type) {
// make sure we are called properly // make sure we are called properly
equal("application/json", type, "The drag&dropped element must have data type application/json"); equal("application/json", type, "The drag&dropped element must have data type application/json");
return JSON.stringify("Example.Node"); return JSON.stringify("Example.Node");
} }
}; };
jsplumb_gadget.props.main.dispatchEvent(e); jsplumb_gadget.props.main.dispatchEvent(e);
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
var node, graph = JSON.parse(content).graph; var node;
var graph = JSON.parse(content).graph;
equal(1, Object.keys(graph.node).length); equal(1, Object.keys(graph.node).length);
node_id = Object.keys(graph.node)[0]; node_id = Object.keys(graph.node)[0];
node = graph.node[node_id]; node = graph.node[node_id];
equal("Example.Node", node._class); equal("Example.Node", node._class);
}).then(function() { }).then(function () {
// click the new node to see display the popup // click the new node to see display the popup
// XXX at the moment nodes have class window // XXX at the moment nodes have class window
equal(1, $("div.window").length, "We have a new node"); equal(1, $("div.window").length, "We have a new node");
$("div.window").simulate("dblclick"); $("div.window").simulate("dblclick");
// Promises that handle the dialog actions are not available // Promises that handle the dialog actions are not available
// immediately after clicking. // immediately after clicking.
var promise = RSVP.Promise(function(resolve) { var promise = new RSVP.Promise(function (resolve) {
var waitForDialogAndDelete = function() { function waitForDialogAndDelete() {
if (!jsplumb_gadget.props.dialog_promise) { if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later. // Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait // XXX this condition is actually incorrect. We need to wait
...@@ -533,11 +549,11 @@ ...@@ -533,11 +549,11 @@
// resolve our test promise once the dialog handling promise is // resolve our test promise once the dialog handling promise is
// finished. // finished.
jsplumb_gadget.props.dialog_promise.then(resolve); jsplumb_gadget.props.dialog_promise.then(resolve);
}; }
waitForDialogAndDelete(); waitForDialogAndDelete();
}); });
return promise.then(function() { return promise.then(function () {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph; var graph = JSON.parse(content).graph;
equal(0, Object.keys(graph.node).length, "node is removed from data"); equal(0, Object.keys(graph.node).length, "node is removed from data");
equal(0, $("div.window").length, "DOM is modified"); equal(0, $("div.window").length, "DOM is modified");
...@@ -547,29 +563,30 @@ ...@@ -547,29 +563,30 @@
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#test-element") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_empty_graph); jsplumb_gadget.render(sample_data_empty_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Graph is automatically layout", function() { test("Graph is automatically layout", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#test-element") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
return jsplumb_gadget.render(sample_data_graph_no_node_coordinate); return jsplumb_gadget.render(sample_data_graph_no_node_coordinate);
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
$.each(JSON.parse(content).graph.node, function(i, node){ /*jslint unparam: true */
ok(node.coordinate.top !== undefined, "Node have top coordinate"); $.each(JSON.parse(content).graph.node, function (ignore, node) {
ok(0 <= node.coordinate.top <= 1, "Node top coordinate is between [0..1]"); ok(node.coordinate.top !== undefined, "Node have top coordinate");
ok(node.coordinate.left !== undefined, "Node have left coordinate"); ok((0 <= node.coordinate.top) && (node.coordinate.top <= 1), "Node top coordinate is between [0..1]");
ok(0 <= node.coordinate.left <= 1, "Node left coordinate is between [0..1]"); ok(node.coordinate.left !== undefined, "Node have left coordinate");
ok((0 <= node.coordinate.left) && (node.coordinate.left <= 1), "Node left coordinate is between [0..1]");
}); });
}).fail(error_handler).always(start); }).fail(error_handler).always(start);
}); });
}); });
})(rJS, JSON, QUnit, RSVP, jQuery); }(rJS, JSON, QUnit, RSVP, jQuery));
\ No newline at end of file \ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment