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

Repair graph editor

graph_editor was using `rsvp.Monitor` in order to dynamically add new promises to the chain of promise in its `declareService` promise. New DOM elements were added during gadget's lifetime and and event handler on these new elements was added to the chain of promise by using [monitor method](
 https://lab.nexedi.com/nexedi/erp5/blob/76ecef89d0b4f6aa6bc5/bt5/erp5_graph_editor/SkinTemplateItem/portal_skins/erp5_graph_editor/dream_graph_editor/jsplumb/jsplumb.js.js#L607) of a monitor instance which was [returned](https://lab.nexedi.com/nexedi/erp5/blob/76ecef89d0b4f6aa6bc5/bt5/erp5_graph_editor/SkinTemplateItem/portal_skins/erp5_graph_editor/dream_graph_editor/jsplumb/jsplumb.js.js#L835) in `declareService`.

`rsvp.js` included in `erp5_xhml_style` exported `Monitor`, but this was removed in  af9c57db . If I understand correctly, this is now included in renderjs, but it's only internal.

So this old way of doing is not longer possible. We realized that instead of dynamically setting `dblclick` event handlers to each graph node elements, we could  simply rely on event bubbling and use a event handler on the parent DOM element. Also, we used renderjs builtin `onEvent` that makes event callback function executed in the promise chain . 

At this stage we did not try to switch all event handling to this approach of using a "global" event handler on the parent DOM, because the goal here was just repairing the graph editor and making sure we have tests running. Also jsplumb uses its own event system.

To enable tests for this:
 - Running the existing qunit test through Zelenium. As far as I know we cannot run qunit test as part of ERP5 test suite.
 - Install the business template in testXHTML so that it is tested by `jsl`, which by the way produce different messages that the jshint integrated in ERP5's code mirror and jslint from `WebScript_checkSyntax`. For now, this passes jshint and jsl, but jslint complains about some indentation and space problems.

/cc  @romain @vincentB @xiaowu.zhang  @seb @gabriel 

/reviewed-on nexedi/erp5!321
parents ff931799 a6cef4a8
......@@ -9,13 +9,17 @@
<script src="renderjs.js" type="text/javascript"></script>
<script src="rsvp.js" type="text/javascript"></script>
-->
<!--
FIXME: Including jQuery twice cause the jsplumb to be loaded twice.
For now we assume that it has already been loaded at this point.
<script src="../lib/jquery.js" type="text/javascript"></script>
-->
<script src="../lib/jquery-ui.js" type="text/javascript"></script>
<script src="../lib/jquery.jsplumb.js" type="text/javascript"></script>
<script src="../lib/handlebars.min.js" type="text/javascript"></script>
<script src="../lib/springy.js" type="text/javascript"></script>
<script src="../dream/mixin_promise.js" type="text/javascript"></script>
<script src="springy.js" type="text/javascript"></script>
<script src="jsplumb.js" type="text/javascript"></script>
<script id="node-template" type="text/x-handlebars-template">
......
/* ===========================================================================
* Copyright 2013-2015 Nexedi SA and Contributors
*
* This file is part of DREAM.
*
* 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DREAM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with DREAM. If not, see <http://www.gnu.org/licenses/>.
* ==========================================================================*/
/*global console, window, RSVP, rJS, $, jsPlumb, Handlebars,
/* ===========================================================================
* Copyright 2013-2015 Nexedi SA and Contributors
*
* This file is part of DREAM.
*
* 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DREAM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with DREAM. If not, see <http://www.gnu.org/licenses/>.
* ==========================================================================*/
/*global console, window, Node, RSVP, rJS, $, jsPlumb, Handlebars,
loopEventListener, promiseEventListener, DOMParser, Springy */
/*jslint unparam: true todo: true */
(function(RSVP, rJS, $, jsPlumb, Handlebars, loopEventListener, promiseEventListener, DOMParser, Springy) {
"use strict";
/* TODO:
* less dependancies ( promise event listner ? )
/*jslint vars: true unparam: true nomen: true todo: true */
(function (RSVP, rJS, $, jsPlumb, Handlebars, loopEventListener, promiseEventListener, DOMParser, Springy) {
"use strict";
/* TODO:
* less dependancies ( promise event listener ? )
* no more handlebars
* id should not always be modifiable
* drop zoom level
* rename draggable()
* factorize node & edge popup edition
*/
/*jslint nomen: true */
var gadget_klass = rJS(window),
domParser = new DOMParser(),
node_template_source = gadget_klass.__template_element.getElementById("node-template").innerHTML,
node_template = Handlebars.compile(node_template_source),
popup_edit_template = gadget_klass.__template_element.getElementById("popup-edit-template").innerHTML;
function layoutGraph(graph_data) {
// Promise returning the graph once springy calculated the layout.
// If the graph already contain layout, return it as is.
function resolver(resolve, reject) {
try {
var springy_graph = new Springy.Graph(),
max_iterations = 100, // we stop layout after 100 iterations.
loop = 0,
springy_nodes = {},
drawn_nodes = {},
min_x=100, max_x=0, min_y=100, max_y=0;
// make a Springy graph with our graph
$.each(graph_data.node, function(key, value) {
if (value.coordinate) {
// graph already has a layout, no need to layout again
return resolve(graph_data);
}
springy_nodes[key] = springy_graph.newNode({node_id: key});
});
$.each(graph_data.edge, function(key, value) {
springy_graph.newEdge(springy_nodes[value.source], springy_nodes[value.destination]);
});
var gadget_klass = rJS(window);
var domParser = new DOMParser();
var node_template_source = gadget_klass.__template_element.getElementById("node-template").innerHTML;
var node_template = Handlebars.compile(node_template_source);
var popup_edit_template = gadget_klass.__template_element.getElementById("popup-edit-template").innerHTML;
function layoutGraph(graph_data) {
// Promise returning the graph once springy calculated the layout.
// If the graph already contain layout, return it as is.
function resolver(resolve, reject) {
try {
var springy_graph = new Springy.Graph();
var max_iterations = 100; // we stop layout after 100 iterations.
var loop = 0;
var springy_nodes = {};
var drawn_nodes = {};
var min_x = 100;
var max_x = 0;
var min_y = 100;
var max_y = 0;
// 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);
}
var layout = new Springy.Layout.ForceDirected(springy_graph, 400.0, 400.0, 0.5);
var renderer = new Springy.Renderer(
layout,
function clear() {},
function drawEdge(edge, p1, p2) {},
function drawNode(node, p) {
drawn_nodes[node.data.node_id] = p;
if ( ++loop > max_iterations) {
renderer.stop();
}
},
function onRenderStop() {
// calculate the min and max of x and y
$.each(graph_data.node, function(key, value) {
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, value) {
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, 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;
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();
}
if (v.destination === node_id) {
v.destination = new_id;
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);
}
gadget.notifyDataChanged();
}
function addEdge(gadget, edge_id, edge_data) {
var overlays = [],
connection;
if (edge_data.name) {
overlays = [
["Label", {
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.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',
// source: 'N1',
// destination: 'N2',
// jsplumb_source_endpoint: 'BottomCenter',
// jsplumb_destination_endpoint: 'LeftMiddle',
// jsplumb_connector: 'Flowchart' }
// Then it is rendered using a flowchart connector. The difficulty is that
// jsplumb does not let you configure the connector type on the edge, but
// on the source endpoint. One solution seem to create all types of
// endpoints on nodes.
if (edge_data.jsplumb_connector === "Flowchart") {
connection = gadget.props.jsplumb_instance.connect({
uuids: [edge_data.source + ".flowChart" + edge_data.jsplumb_source_endpoint,
edge_data.destination + ".flowChart" + edge_data.jsplumb_destination_endpoint
],
overlays: overlays
});
} else {
connection = gadget.props.jsplumb_instance.connect({
source: gadget.props.node_id_to_dom_element_id[edge_data.source],
target: gadget.props.node_id_to_dom_element_id[edge_data.destination],
Connector: ["Bezier", {
curviness: 75
}],
overlays: overlays
});
}
// set data for 'connection' event that will be called "later"
gadget.props.data.graph.edge[edge_id] = edge_data;
// jsplumb assigned an id, but we are controlling ids ourselves.
connection.id = edge_id;
}
function expandSchema(class_definition, full_schema) {
// minimal expanding of json schema, supports merging allOf and $ref
// references
// XXX this should probably be moved to fieldset ( and not handle
// class_definition here)
function resolveReference(ref, schema) {
// 2 here is for #/
var i, ref_path = ref.substr(2, ref.length),
parts = ref_path.split("/");
for (i = 0; i < parts.length; i += 1) {
schema = schema[parts[i]];
}
return schema;
}
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
var referenced,
i,
property,
expanded_class_definition = clone(class_definition) || {};
if (!expanded_class_definition.properties) {
expanded_class_definition.properties = {};
}
// expand direct ref
if (class_definition.$ref) {
referenced = expandSchema(resolveReference(class_definition.$ref, full_schema.class_definition), full_schema);
$.extend(expanded_class_definition, referenced);
delete expanded_class_definition.$ref;
}
// expand ref in properties
for (property in class_definition.properties) {
if (class_definition.properties.hasOwnProperty(property)) {
if (class_definition.properties[property].$ref) {
referenced = expandSchema(resolveReference(class_definition.properties[property].$ref, full_schema.class_definition), full_schema);
$.extend(expanded_class_definition.properties[property], referenced);
delete expanded_class_definition.properties[property].$ref;
} else {
if (class_definition.properties[property].type === "object") {
// no reference, but we expand anyway because we need to recurse in case there is a ref in an object property
referenced = expandSchema(class_definition.properties[property], full_schema);
$.extend(expanded_class_definition.properties[property], referenced);
}
}
}
}
if (class_definition.oneOf) {
expanded_class_definition.oneOf = [];
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) {
referenced = expandSchema(class_definition.allOf[i], full_schema);
if (referenced.properties) {
$.extend(expanded_class_definition.properties, referenced.properties);
delete referenced.properties;
}
$.extend(expanded_class_definition, referenced);
}
if (expanded_class_definition.allOf) {
delete expanded_class_definition.allOf;
}
}
if (expanded_class_definition.$ref) {
delete expanded_class_definition.$ref;
}
return clone(expanded_class_definition);
}
function openEdgeEditionDialog(gadget, connection) {
var edge_id = connection.id,
edge_data = gadget.props.data.graph.edge[edge_id],
edit_popup = $(gadget.props.element).find("#popup-edit-template"),
schema,
fieldset_element,
delete_promise;
schema = expandSchema(gadget.props.data.class_definition[edge_data._class], gadget.props.data);
// We do not edit source & destination on edge this way.
delete schema.properties.source;
delete schema.properties.destination;
gadget.props.element.insertAdjacentHTML("beforeend", popup_edit_template);
edit_popup = $(gadget.props.element).find("#edit-popup");
edit_popup.find(".node_class").text(connection.name || connection._class);
fieldset_element = edit_popup.find("fieldset")[0];
edit_popup.dialog();
edit_popup.show();
function save_promise(fieldset_gadget, edge_id) {
return RSVP.Queue().push(function() {
return promiseEventListener(edit_popup.find(".graph_editor_validate_button")[0], "click", false);
}).push(function(evt) {
var data = {
id: $(evt.target[1]).val(),
data: {}
};
return fieldset_gadget.getContent().then(function(r) {
$.extend(data.data, gadget.props.data.graph.edge[connection.id]);
$.extend(data.data, r);
// to redraw, we remove the edge and add again.
// but we want to disable events on connection, since event
// handling promise are executed asynchronously in undefined order,
// we cannot just remove and /then/ add, because the new edge is
// added before the old is removed.
connection.ignoreEvent = true;
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);
}).push(function() {
// connectionDetached event will remove the edge from data
gadget.props.jsplumb_instance.detach(connection);
});
return gadget.declareGadget("../fieldset/index.html", {
element: fieldset_element,
scope: "fieldset"
}).push(function(fieldset_gadget) {
return RSVP.all([fieldset_gadget, fieldset_gadget.render({
value: edge_data,
property_definition: schema
}, edge_id)]);
}).push(function(fieldset_gadget) {
edit_popup.dialog("open");
return fieldset_gadget[0];
}).push(function(fieldset_gadget) {
fieldset_gadget.startService(); // XXX
return fieldset_gadget;
}).push(function(fieldset_gadget) {
// Expose the dialog handling promise so that we can wait for it in
// test.
gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, edge_id), delete_promise]);
return gadget.props.dialog_promise;
}).push(function() {
edit_popup.dialog("close");
edit_popup.remove();
delete gadget.props.dialog_promise;
});
}
function openNodeEditionDialog(gadget, element) {
var node_id = getNodeId(gadget, element.id),
node_data = gadget.props.data.graph.node[node_id],
node_edit_popup = $(gadget.props.element).find("#popup-edit-template"),
schema,
fieldset_element,
delete_promise;
// If we have no definition for this, we do not allow edition.
// XXX incorrect, we need to display this dialog to be able
// to delete a node
if (gadget.props.data.class_definition[node_data._class] === undefined) {
return;
}
schema = expandSchema(gadget.props.data.class_definition[node_data._class], gadget.props.data);
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");
// Set the name of the popup to the node class
node_edit_popup.find(".node_class").text(node_data.name || node_data._class);
fieldset_element = node_edit_popup.find("fieldset")[0];
node_edit_popup.dialog();
node_data.id = node_id;
function save_promise(fieldset_gadget, node_id) {
return RSVP.Queue().push(function() {
return promiseEventListener(node_edit_popup.find(".graph_editor_validate_button")[0], "click", false);
}).push(function(evt) {
var data = {
// XXX id should not be handled differently ...
id: $(evt.target[1]).val(),
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() {
return removeElement(gadget, node_id);
});
return gadget.declareGadget("../fieldset/index.html", {
element: fieldset_element,
scope: "fieldset"
}).push(function(fieldset_gadget) {
return RSVP.all([fieldset_gadget, fieldset_gadget.render({
value: node_data,
property_definition: schema
}, node_id)]);
}).push(function(fieldset_gadget) {
node_edit_popup.dialog("open");
return fieldset_gadget[0];
}).push(function(fieldset_gadget) {
fieldset_gadget.startService(); // XXX this should not be needed anymore.
return fieldset_gadget;
}).push(function(fieldset_gadget) {
// Expose the dialog handling promise so that we can wait for it in
// test.
gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, node_id), delete_promise]);
return gadget.props.dialog_promise;
}).push(function() {
node_edit_popup.dialog("close");
node_edit_popup.remove();
delete gadget.props.dialog_promise;
});
}
function waitForNodeClick(gadget, node) {
gadget.props.nodes_click_monitor.monitor(loopEventListener(node, "dblclick", false, openNodeEditionDialog.bind(null, gadget, node)));
}
function waitForConnection(gadget) {
return loopJsplumbBind(gadget, "connection", function(info, originalEvent) {
updateConnectionData(gadget, info.connection, false);
});
}
function waitForConnectionDetached(gadget) {
return loopJsplumbBind(gadget, "connectionDetached", function(info, originalEvent) {
updateConnectionData(gadget, info.connection, true);
});
}
function waitForConnectionClick(gadget) {
return loopJsplumbBind(gadget, "click", function(connection) {
return openEdgeEditionDialog(gadget, connection);
});
}
function addNode(gadget, node_id, node_data) {
var render_element = $(gadget.props.main),
class_definition = gadget.props.data.class_definition[node_data._class],
coordinate = node_data.coordinate,
dom_element_id,
box,
absolute_position,
domElement;
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;
if (coordinate === undefined) {
coordinate = {
top: 0,
left: 0
};
}
node_data.coordinate = updateElementCoordinate(gadget, node_id, coordinate);
/*jslint nomen: true*/
domElement = domParser.parseFromString(node_template({
"class": node_data._class.replace(".", "-"),
element_id: dom_element_id,
title: node_data.name || node_data.id,
name: node_data.name || node_data.id
}), "text/html").querySelector(".window");
render_element.append(domElement);
waitForNodeClick(gadget, domElement);
box = $(gadget.props.element).find("#" + dom_element_id);
absolute_position = convertToAbsolutePosition(gadget, coordinate.left, coordinate.top);
if (class_definition && class_definition.css) {
box.css(class_definition.css);
}
box.css("top", absolute_position[1]);
box.css("left", absolute_position[0]);
updateNodeStyle(gadget, dom_element_id);
draggable(gadget);
// XXX make only this element draggable.
// Add some flowchart endpoints
// TODO: add them all !
gadget.props.jsplumb_instance.addEndpoint(dom_element_id, {
isSource: true,
maxConnections: -1,
connector: ["Flowchart", {
stub: [40, 60],
gap: 10,
cornerRadius: 5,
alwaysRespectStubs: true
}]
}, {
anchor: "BottomCenter",
uuid: node_id + ".flowchartBottomCenter"
});
gadget.props.jsplumb_instance.addEndpoint(dom_element_id, {
isTarget: true,
maxConnections: -1
}, {
anchor: "LeftMiddle",
uuid: node_id + ".flowChartLeftMiddle"
});
gadget.notifyDataChanged();
}
function waitForDrop(gadget) {
var callback;
function canceller() {
if (callback !== undefined) {
gadget.props.main.removeEventListener("drop", callback, false);
}
}
/*jslint unparam: true*/
function resolver(resolve, reject) {
callback = function(evt) {
try {
var class_name, offset = $(gadget.props.main).offset(),
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 (e) {
// internet explorer
class_name = JSON.parse(evt.dataTransfer.getData("text"));
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;
}
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;
var canvas_size_x = $(gadget.props.main).width();
var canvas_size_y = $(gadget.props.main).height();
var size_x = $(gadget.props.element).find(".dummy_window").width() * zoom_level;
var size_y = $(gadget.props.element).find(".dummy_window").height() * zoom_level;
var top = Math.floor(y * (canvas_size_y - size_y)) + "px";
var 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;
var canvas_size_x = $(gadget.props.main).width();
var canvas_size_y = $(gadget.props.main).height();
var size_x = $(gadget.props.element).find(".dummy_window").width() * zoom_level;
var size_y = $(gadget.props.element).find(".dummy_window").height() * zoom_level;
var top = Math.max(Math.min(y.replace("px", "") / (canvas_size_y - size_y), 1), 0);
var 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];
var element;
var 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;
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();
}
function addEdge(gadget, edge_id, edge_data) {
var overlays = [];
var connection;
if (edge_data.name) {
overlays = [
["Label", {
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.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',
// source: 'N1',
// destination: 'N2',
// jsplumb_source_endpoint: 'BottomCenter',
// jsplumb_destination_endpoint: 'LeftMiddle',
// jsplumb_connector: 'Flowchart' }
// Then it is rendered using a flowchart connector. The difficulty is that
// jsplumb does not let you configure the connector type on the edge, but
// on the source endpoint. One solution seem to create all types of
// endpoints on nodes.
if (edge_data.jsplumb_connector === "Flowchart") {
connection = gadget.props.jsplumb_instance.connect({
uuids: [
edge_data.source + ".flowChart" + edge_data.jsplumb_source_endpoint,
edge_data.destination + ".flowChart" + edge_data.jsplumb_destination_endpoint
],
overlays: overlays
});
} else {
connection = gadget.props.jsplumb_instance.connect({
source: gadget.props.node_id_to_dom_element_id[edge_data.source],
target: gadget.props.node_id_to_dom_element_id[edge_data.destination],
Connector: ["Bezier", {
curviness: 75
}],
overlays: overlays
});
}
// set data for 'connection' event that will be called "later"
gadget.props.data.graph.edge[edge_id] = edge_data;
// jsplumb assigned an id, but we are controlling ids ourselves.
connection.id = edge_id;
}
function expandSchema(class_definition, full_schema) {
// minimal expanding of json schema, supports merging allOf and $ref
// references
// XXX this should probably be moved to fieldset ( and not handle
// class_definition here)
function resolveReference(ref, schema) {
var i;
var ref_path = ref.substr(2, ref.length); // 2 here is for #/
var parts = ref_path.split("/");
for (i = 0; i < parts.length; i += 1) {
schema = schema[parts[i]];
}
return schema;
}
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
var referenced;
var i;
var property;
var expanded_class_definition = clone(class_definition) || {};
if (!expanded_class_definition.properties) {
expanded_class_definition.properties = {};
}
// expand direct ref
if (class_definition.$ref) {
referenced = expandSchema(resolveReference(class_definition.$ref, full_schema.class_definition), full_schema);
$.extend(expanded_class_definition, referenced);
delete expanded_class_definition.$ref;
}
// expand ref in properties
for (property in class_definition.properties) {
if (class_definition.properties.hasOwnProperty(property)) {
if (class_definition.properties[property].$ref) {
referenced = expandSchema(resolveReference(class_definition.properties[property].$ref, full_schema.class_definition), full_schema);
$.extend(expanded_class_definition.properties[property], referenced);
delete expanded_class_definition.properties[property].$ref;
} else {
if (class_definition.properties[property].type === "object") {
// no reference, but we expand anyway because we need to recurse in case there is a ref in an object property
referenced = expandSchema(class_definition.properties[property], full_schema);
$.extend(expanded_class_definition.properties[property], referenced);
}
}
}
}
if (class_definition.oneOf) {
expanded_class_definition.oneOf = [];
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) {
referenced = expandSchema(class_definition.allOf[i], full_schema);
if (referenced.properties) {
$.extend(expanded_class_definition.properties, referenced.properties);
delete referenced.properties;
}
$.extend(expanded_class_definition, referenced);
}
if (expanded_class_definition.allOf) {
delete expanded_class_definition.allOf;
}
}
if (expanded_class_definition.$ref) {
delete expanded_class_definition.$ref;
}
return clone(expanded_class_definition);
}
function openEdgeEditionDialog(gadget, connection) {
var edge_id = connection.id;
var edge_data = gadget.props.data.graph.edge[edge_id];
var edit_popup = $(gadget.props.element).find("#popup-edit-template");
var schema;
var fieldset_element;
var delete_promise;
schema = expandSchema(gadget.props.data.class_definition[edge_data._class], gadget.props.data);
// We do not edit source & destination on edge this way.
delete schema.properties.source;
delete schema.properties.destination;
gadget.props.element.insertAdjacentHTML("beforeend", popup_edit_template);
edit_popup = $(gadget.props.element).find("#edit-popup");
edit_popup.find(".node_class").text(connection.name || connection._class);
fieldset_element = edit_popup.find("fieldset")[0];
edit_popup.dialog();
edit_popup.show();
function save_promise(fieldset_gadget) {
return new RSVP.Queue().push(function () {
return promiseEventListener(edit_popup.find(".graph_editor_validate_button")[0], "click", false);
}).push(function (evt) {
var data = {
id: $(evt.target[1]).val(),
data: {}
};
return fieldset_gadget.getContent().then(function (r) {
$.extend(data.data, gadget.props.data.graph.edge[connection.id]);
$.extend(data.data, r);
// to redraw, we remove the edge and add again.
// but we want to disable events on connection, since event
// handling promise are executed asynchronously in undefined order,
// we cannot just remove and /then/ add, because the new edge is
// added before the old is removed.
connection.ignoreEvent = true;
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);
}).push(function () {
// connectionDetached event will remove the edge from data
gadget.props.jsplumb_instance.detach(connection);
});
return gadget.declareGadget("../fieldset/index.html", {
element: fieldset_element,
scope: "fieldset"
}).push(function (fieldset_gadget) {
return RSVP.all([fieldset_gadget, fieldset_gadget.render({
value: edge_data,
property_definition: schema
}, edge_id)]);
}).push(function (fieldset_gadget) {
edit_popup.dialog("open");
return fieldset_gadget[0];
}).push(function (fieldset_gadget) {
fieldset_gadget.startService(); // XXX
return fieldset_gadget;
}).push(function (fieldset_gadget) {
// Expose the dialog handling promise so that we can wait for it in
// test.
gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, edge_id), delete_promise]);
return gadget.props.dialog_promise;
}).push(function () {
edit_popup.dialog("close");
edit_popup.remove();
delete gadget.props.dialog_promise;
});
}
function openNodeEditionDialog(gadget, element) {
var node_id = getNodeId(gadget, element.id);
var node_data = gadget.props.data.graph.node[node_id];
var node_edit_popup = $(gadget.props.element).find("#popup-edit-template");
var schema;
var fieldset_element;
var delete_promise;
// If we have no definition for this, we do not allow edition.
// XXX incorrect, we need to display this dialog to be able
// to delete a node
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) {
node_edit_popup.remove();
}
gadget.props.element.insertAdjacentHTML("beforeend", popup_edit_template);
node_edit_popup = $(gadget.props.element).find("#edit-popup");
// Set the name of the popup to the node class
node_edit_popup.find(".node_class").text(node_data.name || node_data._class);
fieldset_element = node_edit_popup.find("fieldset")[0];
node_edit_popup.dialog();
node_data.id = node_id;
function save_promise(fieldset_gadget, node_id) {
return new RSVP.Queue().push(function () {
return promiseEventListener(
node_edit_popup.find(".graph_editor_validate_button")[0],
"click",
false
);
}).push(function (evt) {
var data = {
// XXX id should not be handled differently ...
id: $(evt.target[1]).val(),
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 () {
return removeElement(gadget, node_id);
});
return gadget.declareGadget("../fieldset/index.html", {
element: fieldset_element,
scope: "fieldset"
}).push(function (fieldset_gadget) {
return RSVP.all([
fieldset_gadget,
fieldset_gadget.render(
{
value: node_data,
property_definition: schema
},
node_id
)
]);
}).push(function (fieldset_gadget) {
node_edit_popup.dialog("open");
return fieldset_gadget[0];
}).push(function (fieldset_gadget) {
fieldset_gadget.startService(); // XXX this should not be needed anymore.
return fieldset_gadget;
}).push(function (fieldset_gadget) {
// Expose the dialog handling promise so that we can wait for it in
// test.
gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, node_id), delete_promise]);
return gadget.props.dialog_promise;
}).push(function () {
node_edit_popup.dialog("close");
node_edit_popup.remove();
delete gadget.props.dialog_promise;
});
}
function waitForConnection(gadget) {
return loopJsplumbBind(gadget, "connection", function (info) {
updateConnectionData(gadget, info.connection, false);
});
}
function waitForConnectionDetached(gadget) {
return loopJsplumbBind(gadget, "connectionDetached", function (info) {
updateConnectionData(gadget, info.connection, true);
});
}
function waitForConnectionClick(gadget) {
return loopJsplumbBind(gadget, "click", function (connection) {
return openEdgeEditionDialog(gadget, connection);
});
}
function addNode(gadget, node_id, node_data) {
var render_element = $(gadget.props.main);
var class_definition = gadget.props.data.class_definition[node_data._class];
var coordinate = node_data.coordinate;
var dom_element_id;
var box;
var absolute_position;
var domElement;
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;
if (coordinate === undefined) {
coordinate = {
top: 0,
left: 0
};
}
node_data.coordinate = updateElementCoordinate(gadget, node_id, coordinate);
domElement = domParser.parseFromString(node_template({
"class": node_data._class.replace(".", "-"),
element_id: dom_element_id,
title: node_data.name || node_data.id,
name: node_data.name || node_data.id
}), "text/html").querySelector(".window");
render_element.append(domElement);
box = $(gadget.props.element).find("#" + dom_element_id);
absolute_position = convertToAbsolutePosition(gadget, coordinate.left, coordinate.top);
if (class_definition && class_definition.css) {
box.css(class_definition.css);
}
box.css("top", absolute_position[1]);
box.css("left", absolute_position[0]);
updateNodeStyle(gadget, dom_element_id);
draggable(gadget);
// XXX make only this element draggable.
// Add some flowchart endpoints
// TODO: add them all !
gadget.props.jsplumb_instance.addEndpoint(dom_element_id, {
isSource: true,
maxConnections: -1,
connector: ["Flowchart", {
stub: [40, 60],
gap: 10,
cornerRadius: 5,
alwaysRespectStubs: true
}]
}, {
anchor: "BottomCenter",
uuid: node_id + ".flowchartBottomCenter"
});
gadget.props.jsplumb_instance.addEndpoint(dom_element_id, {
isTarget: true,
maxConnections: -1
}, {
anchor: "LeftMiddle",
uuid: node_id + ".flowChartLeftMiddle"
});
gadget.notifyDataChanged();
}
function waitForDrop(gadget) {
var callback;
function canceller() {
if (callback !== undefined) {
gadget.props.main.removeEventListener("drop", callback, false);
}
}
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);
});
});
}
})
.declareMethod("getContent", function() {
var ret = {};
if (this.props.erp5_key) {
// ERP5
ret[this.props.erp5_key] = JSON.stringify(this.props.data);
return ret;
}
return JSON.stringify(this.props.data);
})
.declareService(function() {
var gadget = this, jsplumb_instance;
this.props.main = this.props.element.querySelector(".graph_container");
this.props.jsplumb_instance = jsplumb_instance = jsPlumb.getInstance();
if (this.props.data) {
// load the data
$.each(this.props.data.graph.node, function(key, value) {
addNode(gadget, key, value);
});
$.each(this.props.data.graph.edge, function(key, value) {
addEdge(gadget, key, value);
});
}
jsplumb_instance.setRenderMode(jsplumb_instance.SVG);
jsplumb_instance.importDefaults({
HoverPaintStyle: {
strokeStyle: "#1e8151",
lineWidth: 2
},
Endpoint: ["Dot", {
radius: 2
}],
ConnectionOverlays: [
["Arrow", {
location: 1,
id: "arrow",
length: 14,
foldback: 0.8
}]
],
Container: this.props.main
});
draggable(gadget);
this.props.nodes_click_monitor = RSVP.Monitor();
return RSVP.all([waitForDrop(gadget),
waitForConnection(gadget),
waitForConnectionDetached(gadget),
waitForConnectionClick(gadget),
gadget.props.nodes_click_monitor
]);
});
})(RSVP, rJS, $, jsPlumb, Handlebars, loopEventListener, promiseEventListener, DOMParser, Springy);
\ No newline at end of file
*/
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);
});
});
}
})
.declareMethod("getContent", function () {
var ret = {};
if (this.props.erp5_key) {
// ERP5
ret[this.props.erp5_key] = JSON.stringify(this.props.data);
return ret;
}
return JSON.stringify(this.props.data);
})
.onEvent("dblclick", function (evt) {
var node = evt.target;
if (
(node.nodeType === Node.ELEMENT_NODE) &&
(node.tagName === "DIV") && node.classList.contains(["window"])
) {
return openNodeEditionDialog(this, node);
}
})
.declareService(function () {
var gadget = this;
var jsplumb_instance;
this.props.main = this.props.element.querySelector(".graph_container");
this.props.jsplumb_instance = jsplumb_instance = jsPlumb.getInstance();
if (this.props.data) {
// load the data
$.each(this.props.data.graph.node, function (key, value) {
addNode(gadget, key, value);
});
$.each(this.props.data.graph.edge, function (key, value) {
addEdge(gadget, key, value);
});
}
jsplumb_instance.setRenderMode(jsplumb_instance.SVG);
jsplumb_instance.importDefaults({
HoverPaintStyle: {
strokeStyle: "#1e8151",
lineWidth: 2
},
Endpoint: ["Dot", {
radius: 2
}],
ConnectionOverlays: [
["Arrow", {
location: 1,
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
......@@ -15,6 +15,8 @@
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<div id="qunit-fixture">
<div id="test-element"/>
</div>
</body>
</html>
/*global window, document, rJS, JSON, QUnit, jQuery, RSVP, console, setTimeout
*/
(function(rJS, JSON, QUnit, RSVP, $) {
/*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, $) {
"use strict";
var start = QUnit.start,
stop = QUnit.stop,
test = QUnit.test,
equal = QUnit.equal,
ok = QUnit.ok,
error_handler = function(e) {
window.console.error(e);
ok(false, e);
},
sample_class_definition = {
var start = QUnit.start;
var stop = QUnit.stop;
var test = QUnit.test;
var equal = QUnit.equal;
var ok = QUnit.ok;
var error_handler = function (e) {
window.console.error(e);
ok(false, e);
};
var sample_class_definition = {
edge: {
description: "Base definition for edge",
properties: {
_class: {
"_class": {
type: "string"
},
destination: {
......@@ -25,7 +24,7 @@
name: {
type: "string"
},
required: [ "name", "_class", "source", "destination" ],
required: ["name", "_class", "source", "destination"],
source: {
type: "string"
}
......@@ -33,35 +32,35 @@
type: "object"
},
"Example.Edge": {
_class: "edge",
allOf: [ {
$ref: "#/edge"
"_class": "edge",
allOf: [{
"$ref": "#/edge"
}, {
properties: {
color: {
"enum": [ "red", "green", "blue" ]
"enum": ["red", "green", "blue"]
}
}
} ],
}],
description: "An example edge with a color property"
},
"Example.Node": {
_class: "node",
allOf: [ {
$ref: "#/node"
"_class": "node",
allOf: [{
"$ref": "#/node"
}, {
properties: {
shape: {
type: "string"
}
}
} ],
}],
description: "An example node with a shape property"
},
node: {
description: "Base definition for node",
properties: {
_class: {
"_class": {
type: "string"
},
coordinate: {
......@@ -74,14 +73,15 @@
name: {
type: "string"
},
required: [ "name", "_class" ]
required: ["name", "_class"]
},
type: "object"
}
}, sample_graph = {
};
var sample_graph = {
edge: {
edge1: {
_class: "Example.Edge",
"_class": "Example.Edge",
source: "N1",
destination: "N2",
color: "blue"
......@@ -89,7 +89,7 @@
},
node: {
N1: {
_class: "Example.Node",
"_class": "Example.Node",
name: "Node 1",
coordinate: {
top: 0,
......@@ -98,7 +98,7 @@
shape: "square"
},
N2: {
_class: "Example.Node",
"_class": "Example.Node",
name: "Node 2",
shape: "circle",
coordinate: {
......@@ -107,127 +107,161 @@
}
}
}
}, sample_graph_not_connected = {
};
var sample_graph_no_node_coodinate = {
edge: {
edge1: {
"_class": "Example.Edge",
source: "N1",
destination: "N2",
color: "blue"
}
},
node: {
N1: {
"_class": "Example.Node",
name: "Node 1",
shape: "square"
},
N2: {
"_class": "Example.Node",
name: "Node 2",
shape: "circle"
}
}
};
var sample_graph_not_connected = {
edge: {},
node: {
N1: {
_class: "Example.Node",
"_class": "Example.Node",
name: "Node 1",
shape: "square"
},
N2: {
_class: "Example.Node",
"_class": "Example.Node",
name: "Node 2",
shape: "circle"
}
}
}, sample_data_graph = JSON.stringify({
};
var sample_data_graph = JSON.stringify({
class_definition: sample_class_definition,
graph: sample_graph
}), sample_data_graph_not_connected = JSON.stringify({
});
var sample_data_graph_no_node_coordinate = JSON.stringify({
class_definition: sample_class_definition,
graph: sample_graph_no_node_coodinate
});
var sample_data_graph_not_connected = JSON.stringify({
class_definition: sample_class_definition,
graph: sample_graph_not_connected
}), sample_data_empty_graph = JSON.stringify({
});
var sample_data_empty_graph = JSON.stringify({
class_definition: sample_class_definition,
graph: {
node: {},
edge: {}
}
});
QUnit.config.testTimeout = 60000;
rJS(window).ready(function(g) {
test("Sample graph can be loaded and output is equal to input", function() {
rJS(window).ready(function (g) {
test("Sample graph can be loaded and output is equal to input", function () {
var jsplumb_gadget;
stop();
g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture")
}).then(function(new_gadget) {
element: document.querySelector("#test-element")
}).then(function (new_gadget) {
jsplumb_gadget = new_gadget;
return jsplumb_gadget.render(sample_data_graph);
}).then(function() {
}).then(function () {
return jsplumb_gadget.getContent();
}).then(function(content) {
}).then(function (content) {
equal(content, sample_data_graph);
}).fail(error_handler).always(start);
});
test("New node can be drag & dropped", function() {
test("New node can be drag & dropped", function () {
var jsplumb_gadget;
stop();
function runTest() {
// XXX here I used getContent to have a promise, but there must be a
// more elegant way.
return jsplumb_gadget.getContent().then(function() {
return jsplumb_gadget.getContent().then(function () {
// fake a drop event
var e = new window.Event("drop");
e.dataTransfer = {
getData: function(type) {
getData: function (type) {
// make sure we are called properly
equal("application/json", type, "The drag&dropped element must have data type application/json");
return JSON.stringify("Example.Node");
}
};
jsplumb_gadget.props.main.dispatchEvent(e);
}).then(function() {
}).then(function () {
return jsplumb_gadget.getContent();
}).then(function(content) {
var node, graph = JSON.parse(content).graph;
}).then(function (content) {
var node;
var graph = JSON.parse(content).graph;
equal(1, Object.keys(graph.node).length, "There is one new node class");
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", {
element: document.querySelector("#qunit-fixture")
}).then(function(new_gadget) {
element: document.querySelector("#test-element")
}).then(function (new_gadget) {
jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_empty_graph);
}).then(runTest).fail(error_handler).always(start);
});
test("Node can be dragged", function() {
test("Node can be dragged", function () {
var jsplumb_gadget;
stop();
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
// might change )
$("div[title='Node 1']").simulate("drag", {
dx: 100,
dy: 60
});
}).then(function() {
}).then(function () {
return jsplumb_gadget.getContent();
}).then(function(content) {
var graph = JSON.parse(content).graph, node_coordinate = graph.node.N1.coordinate;
}).then(function (content) {
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
// as we moved 10%
ok(node_coordinate.top - .1 < .1, "Top is ok");
ok(node_coordinate.left - .1 < .1, "Left is ok");
ok(node_coordinate.top - 0.1 < 0.1, "Top is ok");
ok(node_coordinate.left - 0.1 < 0.1, "Left is ok");
});
}
g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture")
}).then(function(new_gadget) {
element: document.querySelector("#test-element")
}).then(function (new_gadget) {
jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph);
}).then(runTest).fail(error_handler).always(start);
});
test("Node properties can be edited", function() {
test("Node properties can be edited", function () {
var jsplumb_gadget;
stop();
function runTest() {
return jsplumb_gadget.getContent().then(function() {
return jsplumb_gadget.getContent().then(function () {
// click on a node to see display the popup
$("div[title='Node 1']").simulate("dblclick");
// Promises that handle the dialog actions are not available
// immediately after clicking.
var promise = RSVP.Promise(function(resolve) {
var fillDialog = function() {
var promise = new RSVP.Promise(function (resolve) {
function fillDialog() {
if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait
// for the event listener to have been registered for the
// dialog buttons. This setTimeout is good enough for now.
return setTimeout(fillDialog, 1e3);
setTimeout(fillDialog, 1e3);
return;
}
// check displayed values
equal($("input[name='id']").val(), "N1");
......@@ -241,12 +275,13 @@
// resolve our test promise once the dialog handling promise is
// finished.
jsplumb_gadget.props.dialog_promise.then(resolve);
};
}
fillDialog();
});
return promise.then(function() {
return jsplumb_gadget.getContent().then(function(content) {
var graph = JSON.parse(content).graph, node = graph.node.N1;
return promise.then(function () {
return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph;
var node = graph.node.N1;
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(1, $("div[title='Modified Name']").length, "DOM title attribute is modified");
......@@ -255,27 +290,29 @@
});
}
g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture")
}).then(function(new_gadget) {
element: document.querySelector("#test-element")
}).then(function (new_gadget) {
jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph);
}).then(runTest).fail(error_handler).always(start);
});
test("Node can be connected", function() {
test("Node can be connected", function () {
var jsplumb_gadget;
stop();
function runTest() {
return jsplumb_gadget.getContent().then(function(content) {
var node1 = jsplumb_gadget.props.main.querySelector("div[title='Node 1']"), node2 = jsplumb_gadget.props.main.querySelector("div[title='Node 2']");
return jsplumb_gadget.getContent().then(function (content) {
var node1 = jsplumb_gadget.props.main.querySelector("div[title='Node 1']");
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");
jsplumb_gadget.props.jsplumb_instance.connect({
source: node1.id,
target: node2.id
});
}).then(function() {
}).then(function () {
return jsplumb_gadget.getContent();
}).then(function(content) {
var edge, graph = JSON.parse(content).graph;
}).then(function (content) {
var edge;
var graph = JSON.parse(content).graph;
equal(2, Object.keys(graph.node).length, "We still have 2 nodes");
equal(1, Object.keys(graph.edge).length, "We have 1 edge");
edge = graph.edge[Object.keys(graph.edge)[0]];
......@@ -286,42 +323,43 @@
});
}
g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture")
}).then(function(new_gadget) {
element: document.querySelector("#test-element")
}).then(function (new_gadget) {
jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph_not_connected);
}).then(runTest).fail(error_handler).always(start);
});
test("Node can be deleted", function() {
test("Node can be deleted", function () {
var jsplumb_gadget;
stop();
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, $("._jsPlumb_connector").length, "there is 1 connection");
// click on node 1 to see display the popup
$("div[title='Node 1']").simulate("dblclick");
// Promises that handle the dialog actions are not available
// immediately after clicking.
var promise = RSVP.Promise(function(resolve) {
var waitForDialogAndDelete = function() {
var promise = new RSVP.Promise(function (resolve) {
function waitForDialogAndDelete() {
if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait
// for the event listener to have been registered for the
// dialog buttons. This setTimeout is good enough for now.
return setTimeout(waitForDialogAndDelete, 1e3);
setTimeout(waitForDialogAndDelete, 1e3);
return;
}
equal(1, $("input[value='Delete']").length, "There should be one delete button");
$("input[value='Delete']").click();
// resolve our test promise once the dialog handling promise is
// finished.
jsplumb_gadget.props.dialog_promise.then(resolve);
};
}
waitForDialogAndDelete();
});
return promise.then(function() {
return jsplumb_gadget.getContent().then(function(content) {
return promise.then(function () {
return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph;
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");
......@@ -332,29 +370,30 @@
});
}
g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture")
}).then(function(new_gadget) {
element: document.querySelector("#test-element")
}).then(function (new_gadget) {
jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph);
}).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;
stop();
function runTest() {
return jsplumb_gadget.getContent().then(function() {
return jsplumb_gadget.getContent().then(function () {
// click on a node to see display the popup
$("div[title='Node 1']").simulate("dblclick");
// Promises that handle the dialog actions are not available
// immediately after clicking.
var promise = RSVP.Promise(function(resolve) {
var fillDialog = function() {
var promise = new RSVP.Promise(function (resolve) {
function fillDialog() {
if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait
// for the event listener to have been registered for the
// dialog buttons. This setTimeout is good enough for now.
return setTimeout(fillDialog, 1e3);
setTimeout(fillDialog, 1e3);
return;
}
equal($("input[name='id']").val(), "N1");
// change the id
......@@ -364,11 +403,11 @@
// resolve our test promise once the dialog handling promise is
// finished.
jsplumb_gadget.props.dialog_promise.then(resolve);
};
}
fillDialog();
});
return promise.then(function() {
return jsplumb_gadget.getContent().then(function(content) {
return promise.then(function () {
return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph;
equal(2, Object.keys(graph.node).length, "We still have two nodes");
ok(graph.node.N1b !== undefined, "Node Id changed");
......@@ -379,52 +418,55 @@
});
}
g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture")
}).then(function(new_gadget) {
element: document.querySelector("#test-element")
}).then(function (new_gadget) {
jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph);
}).then(runTest).fail(error_handler).always(start);
});
test("New node can be edited", function() {
var jsplumb_gadget, node_id;
test("New node can be edited", function () {
var jsplumb_gadget;
var node_id;
stop();
function runTest() {
// XXX here I used getContent to have a promise, but there must be a
// more elegant way.
return jsplumb_gadget.getContent().then(function() {
return jsplumb_gadget.getContent().then(function () {
// fake a drop event
var e = new window.Event("drop");
e.dataTransfer = {
getData: function(type) {
getData: function (type) {
// make sure we are called properly
equal("application/json", type, "The drag&dropped element must have data type application/json");
return JSON.stringify("Example.Node");
}
};
jsplumb_gadget.props.main.dispatchEvent(e);
}).then(function() {
}).then(function () {
return jsplumb_gadget.getContent();
}).then(function(content) {
var node, graph = JSON.parse(content).graph;
}).then(function (content) {
var node;
var graph = JSON.parse(content).graph;
equal(1, Object.keys(graph.node).length);
node_id = Object.keys(graph.node)[0];
node = graph.node[node_id];
equal("Example.Node", node._class);
}).then(function() {
}).then(function () {
// click the new node to see display the popup
// XXX at the moment nodes have class window
equal(1, $("div.window").length, "We have a new node");
$("div.window").simulate("dblclick");
// Promises that handle the dialog actions are not available
// immediately after clicking.
var promise = RSVP.Promise(function(resolve) {
var fillDialog = function() {
var promise = new RSVP.Promise(function (resolve) {
function fillDialog() {
if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait
// for the event listener to have been registered for the
// dialog buttons. This setTimeout is good enough for now.
return setTimeout(fillDialog, 1e3);
setTimeout(fillDialog, 1e3);
return;
}
// check displayed values
equal($("input[name='id']").val(), node_id);
......@@ -438,12 +480,13 @@
// resolve our test promise once the dialog handling promise is
// finished.
jsplumb_gadget.props.dialog_promise.then(resolve);
};
}
fillDialog();
});
return promise.then(function() {
return jsplumb_gadget.getContent().then(function(content) {
var graph = JSON.parse(content).graph, node = graph.node[node_id];
return promise.then(function () {
return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph;
var node = graph.node[node_id];
equal("Modified Name", node.name, "Data is modified");
equal("Modified Name", $("div.window").text(), "DOM is modified");
});
......@@ -451,63 +494,66 @@
});
}
g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture")
}).then(function(new_gadget) {
element: document.querySelector("#test-element")
}).then(function (new_gadget) {
jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_empty_graph);
}).then(runTest).fail(error_handler).always(start);
});
test("New node can be deleted", function() {
var jsplumb_gadget, node_id;
test("New node can be deleted", function () {
var jsplumb_gadget;
var node_id;
stop();
function runTest() {
// XXX here I used getContent to have a promise, but there must be a
// more elegant way.
return jsplumb_gadget.getContent().then(function() {
return jsplumb_gadget.getContent().then(function () {
// fake a drop event
var e = new window.Event("drop");
e.dataTransfer = {
getData: function(type) {
getData: function (type) {
// make sure we are called properly
equal("application/json", type, "The drag&dropped element must have data type application/json");
return JSON.stringify("Example.Node");
}
};
jsplumb_gadget.props.main.dispatchEvent(e);
}).then(function() {
}).then(function () {
return jsplumb_gadget.getContent();
}).then(function(content) {
var node, graph = JSON.parse(content).graph;
}).then(function (content) {
var node;
var graph = JSON.parse(content).graph;
equal(1, Object.keys(graph.node).length);
node_id = Object.keys(graph.node)[0];
node = graph.node[node_id];
equal("Example.Node", node._class);
}).then(function() {
}).then(function () {
// click the new node to see display the popup
// XXX at the moment nodes have class window
equal(1, $("div.window").length, "We have a new node");
$("div.window").simulate("dblclick");
// Promises that handle the dialog actions are not available
// immediately after clicking.
var promise = RSVP.Promise(function(resolve) {
var waitForDialogAndDelete = function() {
var promise = new RSVP.Promise(function (resolve) {
function waitForDialogAndDelete() {
if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait
// for the event listener to have been registered for the
// dialog buttons. This setTimeout is good enough for now.
return setTimeout(waitForDialogAndDelete, 1e3);
setTimeout(waitForDialogAndDelete, 1e3);
return;
}
equal(1, $("input[value='Delete']").length, "There should be one delete button");
$("input[value='Delete']").click();
// resolve our test promise once the dialog handling promise is
// finished.
jsplumb_gadget.props.dialog_promise.then(resolve);
};
}
waitForDialogAndDelete();
});
return promise.then(function() {
return jsplumb_gadget.getContent().then(function(content) {
return promise.then(function () {
return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph;
equal(0, Object.keys(graph.node).length, "node is removed from data");
equal(0, $("div.window").length, "DOM is modified");
......@@ -516,11 +562,31 @@
});
}
g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture")
}).then(function(new_gadget) {
element: document.querySelector("#test-element")
}).then(function (new_gadget) {
jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_empty_graph);
}).then(runTest).fail(error_handler).always(start);
});
test("Graph is automatically layout", function () {
var jsplumb_gadget;
stop();
g.declareGadget("./index.html", {
element: document.querySelector("#test-element")
}).then(function (new_gadget) {
jsplumb_gadget = new_gadget;
return jsplumb_gadget.render(sample_data_graph_no_node_coordinate);
}).then(function () {
return jsplumb_gadget.getContent();
}).then(function (content) {
/*jslint unparam: true */
$.each(JSON.parse(content).graph.node, function (ignore, node) {
ok(node.coordinate.top !== undefined, "Node have top coordinate");
ok((0 <= node.coordinate.top) && (node.coordinate.top <= 1), "Node top 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);
});
});
})(rJS, JSON, QUnit, RSVP, jQuery);
\ No newline at end of file
}(rJS, JSON, QUnit, RSVP, jQuery));
\ No newline at end of file
......@@ -14,16 +14,16 @@
}).declareMethod("render", function(options) {
var select = this.element.getElementsByTagName("select")[0], i, template, tmp = "";
select.setAttribute("name", options.key);
for (i = 0; i < options.property_definition.enum.length; i += 1) {
if (options.property_definition.enum[i] === options.value) {
for (i = 0; i < options.property_definition['enum'].length; i += 1) {
if (options.property_definition['enum'][i] === options.value) {
template = selected_option_template;
} else {
template = option_template;
}
// XXX value and text are always same in json schema
tmp += template({
value: options.property_definition.enum[i],
text: options.property_definition.enum[i]
value: options.property_definition['enum'][i],
text: options.property_definition['enum'][i]
});
}
select.innerHTML += tmp;
......
erp5_trade
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Zuite" module="Products.Zelenium.zuite"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>graph_editor_zuite</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testQunit</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode>Graph Editor Qunit Test</unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html>
<head><title>Graph Editor Qunit Test</title></head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="4">
Run existing qunit test in zelenium framework, to easily integrate it in current test suite.
</td></tr>
</thead><tbody>
<tr>
<td>open</td>
<td tal:content="string:${context/portal_url}/dream_graph_editor/jsplumb/test.html"></td>
<td></td>
</tr>
<tr>
<td>waitForTextPresent</td>
<td>Tests completed in </td>
<td>30000</td>
</tr>
<tr>
<td>assertText</td>
<td>css=#qunit-testresult span.failed</td>
<td>0</td>
</tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
##############################################################################
#
# Copyright (c) 2017 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import unittest
from Products.ERP5Type.tests.ERP5TypeFunctionalTestCase import ERP5TypeFunctionalTestCase
class TestGraphEditor(ERP5TypeFunctionalTestCase):
run_only = "graph_editor_zuite"
def getBusinessTemplateList(self):
return (
'erp5_graph_editor',
'erp5_graph_editor_ui_test',
'erp5_ui_test_core',
)
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestGraphEditor))
return suite
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>testFunctionalGraphEditor</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testFunctionalGraphEditor</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Test Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData>
erp5_graph_editor
erp5_ui_test_core
\ No newline at end of file
portal_tests/graph_editor_zuite
portal_tests/graph_editor_zuite/**
\ No newline at end of file
test.erp5.testFunctionalGraphEditor
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_graph_editor_ui_test
\ No newline at end of file
......@@ -53,7 +53,15 @@ class TestXHTMLMixin(ERP5TypeTestCase):
'renderjs.js','jio.js','rsvp.js','handlebars.js',
'pdf_js/build/pdf.js', 'pdf_js/build/pdf.worker.js',
'pdf_js/compatibility.js', 'pdf_js/debugger.js',
'pdf_js/viewer.js', 'pdf_js/l10n.js')
'pdf_js/viewer.js', 'pdf_js/l10n.js',
'dream_graph_editor/lib/handlebars.min.js',
'dream_graph_editor/lib/jquery-ui.js',
'dream_graph_editor/lib/jquery.js',
'dream_graph_editor/lib/jquery.jsplumb.js',
'dream_graph_editor/lib/jquery.simulate.js',
'dream_graph_editor/lib/qunit.js',
'dream_graph_editor/lib/springy.js',
)
JSL_IGNORE_SKIN_LIST = ('erp5_ace_editor', 'erp5_code_mirror',
'erp5_fckeditor', 'erp5_jquery', 'erp5_jquery_ui',
'erp5_svg_editor', 'erp5_xinha_editor')
......@@ -429,6 +437,7 @@ class TestXHTML(TestXHTMLMixin):
'erp5_xinha_editor',
'erp5_svg_editor',
'erp5_jquery_sheet_editor',
'erp5_graph_editor',
'erp5_web_ung_core',
'erp5_web_ung_theme',
'erp5_web_ung_role',
......
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