workflows are now loaded via server requests

This commit is contained in:
David Mosbach 2023-05-29 18:26:12 +02:00
parent dfb4a8f0bb
commit c242c4a39a

631
editor.js
View File

@ -48,150 +48,29 @@ function openSearchMenu(menuitem) {
var workflow = {} var workflow = {}
// fetch('./test.json') fetch('http://localhost:8080/test.json')
// .then((response) => response.json()) .then((response) => response.json())
// .then((data) => { .then((data) => {
// for (var key in data) for (var key in data)
// workflow[key] = data[key]; workflow[key] = data[key];
// }); runnn();
});
// Counters for placeholder IDs of states/actions added via GUI
var stateIdCounter = workflow.states ? workflow.states.length : 0;
var actionIdCounter = workflow.states ? workflow.actions.length : 0;
//Parse workflow
workflow.states.forEach(state => {
var messages = [];
state.stateData.messages.forEach(msg => messages.push(new Message(msg)));
state.stateData.messages = messages;
var viewers = [];
state.stateData.viewers.forEach(v => viewers.push(new Role(v)));
state.stateData.viewers = viewers;
state.stateData.payload = new Payload(state.stateData.payload);
})
workflow.actions.forEach(action => {
var messages = [];
action.actionData.messages.forEach(msg => messages.push(new Message(msg)));
action.actionData.messages = messages;
var viewers = [];
action.actionData.viewers.forEach(v => viewers.push(new Role(v)));
action.actionData.viewers = viewers;
var actors = [];
action.actionData.actors.forEach(v => actors.push(new Role(v)));
action.actionData.actors = actors;
var viewActors = [];
action.actionData['actor Viewers'].forEach(v => viewActors.push(new Role(v)));
action.actionData['actor Viewers'] = viewActors;
action.actionData.form = new Payload(action.actionData.form);
})
//Actors of the workflow //Actors of the workflow
var actors = []; var actors = [];
workflow.actions.forEach(act => act.actionData.actors.forEach(a => {
var includes = false;
actors.forEach(actor => includes = includes || equalRoles(a, actor));
(!includes) && actors.push(a);
(!act.actionData.actorNames) && (act.actionData.actorNames = []);
act.actionData.actorNames.push(getRoleName(a));
}));
// console.log(actors);
// workflow.actions.forEach(a => console.log(a.actionData.actorNames));
function getRoleName(role) {
if (typeof role == 'string') {
return role;
} else if (role instanceof Role) {
return role.name;
} else {
return JSON.stringify(role);
}
}
const NO_ACTOR = 'None';
//Prepare actor highlighting
const selectedActor = document.getElementById('actor'); const selectedActor = document.getElementById('actor');
var allActors = document.createElement('option');
allActors.text = NO_ACTOR;
selectedActor.add(allActors);
actors.forEach(actor => {
var option = document.createElement('option');
option.text = getRoleName(actor);
selectedActor.add(option);
});
//Viewers of the workflow //Viewers of the workflow
var viewers = []; var viewers = [];
const selectedViewer = document.getElementById('viewer');
//Actions/States with no explicit viewers //Actions/States with no explicit viewers
var viewableByAll = [] var viewableByAll = []
//Possible initiators //Possible initiators
var initiators = [] var initiators = []
//Implicit state from which initial actions can be selected //Implicit state from which initial actions can be selected
var initState = null; var initState = null;
//Identify all viewers of every action const NO_ACTOR = 'None';
workflow.actions.forEach(act => {
if (act.actionData.viewers.length === 0) {
viewableByAll.push(act.actionData);
} else {
act.actionData.viewers.forEach(v => {
var includes = false;
viewers.forEach(viewer => includes = includes || equalRoles(v, viewer));
(!includes) && viewers.push(v);
(!act.actionData.viewerNames) && (act.actionData.viewerNames = []);
act.actionData.viewerNames.push(getRoleName(v));
})
}
if (act.actionData.mode === 'initial') {
act.actionData.actorNames.forEach(an => !initiators.includes(an) && initiators.push(an));
}
});
//Identify all viewers of every state
workflow.states.forEach(st => {
if (st.name === '@@INIT') {
initState = st;
} else if (st.stateData.viewers.length === 0) {
viewableByAll.push(st.stateData);
} else {
st.stateData.viewers.forEach(v => {
var includes = false;
viewers.forEach(viewer => includes = includes || equalRoles(v, viewer));
(!includes) && viewers.push(v);
(!st.stateData.viewerNames) && (st.stateData.viewerNames = []);
st.stateData.viewerNames.push(getRoleName(v));
})
}
});
initState.stateData.viewerNames = initiators;
const ALL_VIEW = "Not explicitly specified";
if (viewableByAll.length > 0) {
viewers.push(ALL_VIEW);
var viewerNames = []
viewers.forEach(viewer => viewerNames.push(getRoleName(viewer)));
viewableByAll.forEach(data => {
data.viewerNames = viewerNames;
});
}
const NO_VIEWER = NO_ACTOR; const NO_VIEWER = NO_ACTOR;
//Prepare viewer highlighting
const selectedViewer = document.getElementById('viewer');
var allViewers = document.createElement('option');
allViewers.text = NO_VIEWER;
selectedViewer.add(allViewers);
viewers.forEach(viewer => {
var option = document.createElement('option');
option.text = getRoleName(viewer);
selectedViewer.add(option);
});
//source & target nodes of all currently highlighted actions //source & target nodes of all currently highlighted actions
var highlightedSources = []; var highlightedSources = [];
var highlightedTargets = []; var highlightedTargets = [];
@ -223,12 +102,6 @@ function selectViewer() {
}); });
} }
var selfLoops = {}; // All edges whose targets equal their sources.
var overlappingEdges = {}; // All edges whose target and source are connected by further.
const selfLoopCurvMin = 0.5; // Minimum curvature of a self loop.
const curvatureMinMax = 0.2; // Minimum/maximum curvature (1 +/- x) of overlapping edges.
var selection = null; // The currently selected node/edge. var selection = null; // The currently selected node/edge.
var rightSelection = null; // The currently right clicked node/edge. var rightSelection = null; // The currently right clicked node/edge.
var edgeTo = null; // Target of an edge to be created. var edgeTo = null; // Target of an edge to be created.
@ -241,35 +114,179 @@ const sideButtons = document.getElementById('sidebuttons');
const contextMenuBg = document.getElementById('ctmenubg'); //Click on background const contextMenuBg = document.getElementById('ctmenubg'); //Click on background
const contextMenuSt = document.getElementById('ctmenust'); //Click on state const contextMenuSt = document.getElementById('ctmenust'); //Click on state
const contextMenuEd = document.getElementById('ctmenued'); //Click on edge const contextMenuEd = document.getElementById('ctmenued'); //Click on edge
// Counters for placeholder IDs of states/actions added via GUI
var stateIdCounter = 0;
var actionIdCounter = 0;
var stateAbbreviations = [];
var newStateCoords = {'x': 0, 'y': 0}; //Initial coordinates of the next new state
const edgeColourDefault = '#999999ff'; /**
const edgeColourSelected = '#000000ff'; * Marks the given item as selected.
const edgeColourHighlightDefault = '#6ed4d4'; * @param {*} item The node or edge to select.
const edgeColourHighlightSelected = 'magenta'; */
const edgeColourSubtleDefault = '#99999955'; function select(item) {
const edgeColourSubtleSelected = '#00000055'; contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
edgeFrom = edgeTo = rightSelection = null;
selection = selection === item ? null : item;
if (selection === item) {
while (sideContent.firstChild)
sideContent.removeChild(sideContent.lastChild);
sidePanel.style.display = 'block'
sideHeading.innerHTML = item.name;
var data = document.createElement('div');
var content = generatePanelContent(selection);
content.forEach(c => data.appendChild(c));
sideContent.appendChild(data);
var spStyle = window.getComputedStyle(sidePanel);
var shStyle = window.getComputedStyle(sideHeading);
sideContent.style.top = sideHeading.offsetHeight + parseFloat(spStyle.paddingTop) + parseFloat(shStyle.marginTop) + parseFloat(shStyle.marginBottom);
var sbStyle = window.getComputedStyle(sideButtons);
sideContent.style.bottom = sideButtons.offsetHeight + parseFloat(spStyle.paddingBottom) + parseFloat(sbStyle.marginTop) + parseFloat(sbStyle.marginBottom);
// console.log(sideHeading.offsetHeight + shStyle.marginTop + shStyle.marginBottom);
} else {
sidePanel.style.display = 'none';
}
console.log(item);
}
function deselect() {
sidePanel.style.display = 'none';
selection = null;
}
function rightSelect() {
select(rightSelection);
}
/**
* Adds a new state to the workflow and auto-selects it.
*/
function addState() {
var nodeId = stateIdCounter ++;
var x = newStateCoords.x;
var y = newStateCoords.y;
state = {id: nodeId, x: x, y: y, name: 'state_' + nodeId, fx: x, fy: y, val: 5};
workflow.states.push(state);
updateGraph();
select(state);
}
/**
* Adds a new action between two states.
* @param {*} source The source state.
* @param {*} target The target state.
*/
function connect(source, target) {
let linkId = actionIdCounter ++;
action = {id: linkId, source: source, target: target, name: 'action_' + linkId, actionData: {
viewerNames: [], actorNames: []
}};
workflow.actions.push(action);
updateGraph();
select(action);
}
function markEdgeTo() {
edgeTo = rightSelection;
contextMenuSt.style.display = 'none';
}
function markEdgeFrom() {
edgeFrom = rightSelection;
contextMenuSt.style.display = 'none';
}
function removeSelection() {
if (selection) {
if (selection.actionData) removeAction(selection);
else removeState(selection);
deselect();
edgeFrom = edgeTo = rightSelection = null;
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
}
}
function removeRightSelection() {
if (rightSelection) {
if (rightSelection.actionData) removeAction(rightSelection);
else removeState(rightSelection);
if (selection === rightSelection) deselect();
edgeFrom = edgeTo = rightSelection = null;
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
}
}
function generatePanelContent(selection) {
var children = [];
var data = selection.stateData || selection.actionData
for (var key in data) {
if (key === 'viewerNames' || key === 'actorNames') continue;
var h = document.createElement('h2');
var heading = document.createTextNode(key.substring(0,1).toUpperCase() + key.substring(1));
h.appendChild(heading);
children.push(h);
var content = data[key];
if (content instanceof Array && content.length > 0 && content[0] instanceof Message) {
content.forEach(msg => msg.format().forEach(child => children.push(child)));
} else if (content instanceof Payload) {
content.format().forEach(child => children.push(child));
} else if (content instanceof Array && content.length > 0 && content[0] instanceof Role) {
var viewerList = document.createElement('ul');
content.forEach(viewer => {
var v = document.createElement('li');
v.appendChild(document.createTextNode(viewer.name));
viewerList.appendChild(v);
});
children.push(viewerList);
} else {
var p = document.createElement('p');
var text = document.createTextNode(JSON.stringify(data[key]));
p.appendChild(text);
children.push(p);
}
}
return children;
}
/**
* Removes an edge from the workflow.
* @param {*} action The action to remove.
*/
function removeAction(action) {
workflow.actions.splice(workflow.actions.indexOf(action), 1);
}
/** /**
* Checks if two roles are equal. * Removes a state from the workflow.
* @param {*} role1 * @param {*} state The state to remove.
* @param {*} role2
* @returns
*/ */
function equalRoles(role1, role2) { function removeState(state) {
role1 instanceof Role && (role1 = role1.json); workflow.actions
role2 instanceof Role && (role2 = role2.json); .filter(edge => edge.source === state || edge.target === state)
var equal = role1.tag === role2.tag; .forEach(edge => removeAction(edge));
if (role1.tag == 'payload-reference') { workflow.states.splice(workflow.states.indexOf(state), 1);
equal = equal && (role1['payload-label'] === role2['payload-label']); var abbreviation = state.stateData && state.stateData.abbreviation;
} else if (role1.tag == 'user') { abbreviation && stateAbbreviations.splice(stateAbbreviations.indexOf(abbreviation), 1);
equal = equal && (role1.user === role2.user);
} else if (role1.tag == 'authorized') {
equal = equal && (role1.authorized['dnf-terms'][0][0].var === role2.authorized['dnf-terms'][0][0].var);
}
return equal;
} }
var selfLoops = {}; // All edges whose targets equal their sources.
var overlappingEdges = {}; // All edges whose target and source are connected by further.
const selfLoopCurvMin = 0.5; // Minimum curvature of a self loop.
const curvatureMinMax = 0.2; // Minimum/maximum curvature (1 +/- x) of overlapping edges.
const wfGraph = ForceGraph()
/**
* Updates the nodes and edges of the workflow graph.
*/
function updateGraph() {
identifyOverlappingEdges()
computeCurvatures()
wfGraph(document.getElementById('graph')).graphData({nodes: workflow.states, links: workflow.actions});
}
/** /**
* Identifies and stores self loops as well as overlapping edges (i.e. multiple edges sharing the * Identifies and stores self loops as well as overlapping edges (i.e. multiple edges sharing the
@ -319,170 +336,153 @@ function computeCurvatures() {
} }
function generatePanelContent(selection) {
var children = [];
var data = selection.stateData || selection.actionData
for (var key in data) {
if (key === 'viewerNames' || key === 'actorNames') continue;
var h = document.createElement('h2');
var heading = document.createTextNode(key.substring(0,1).toUpperCase() + key.substring(1));
h.appendChild(heading);
children.push(h);
var content = data[key];
if (content instanceof Array && content.length > 0 && content[0] instanceof Message) {
content.forEach(msg => msg.format().forEach(child => children.push(child)));
} else if (content instanceof Payload) {
content.format().forEach(child => children.push(child));
} else if (content instanceof Array && content.length > 0 && content[0] instanceof Role) {
var viewerList = document.createElement('ul');
content.forEach(viewer => {
var v = document.createElement('li');
v.appendChild(document.createTextNode(viewer.name));
viewerList.appendChild(v);
});
children.push(viewerList);
} else {
var p = document.createElement('p');
var text = document.createTextNode(JSON.stringify(data[key]));
p.appendChild(text);
children.push(p);
}
}
return children;
}
/** function runnn() {
* Marks the given item as selected.
* @param {*} item The node or edge to select. stateIdCounter = workflow.states ? workflow.states.length : 0;
*/ actionIdCounter = workflow.states ? workflow.actions.length : 0;
function select(item) {
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none'; //Parse workflow
edgeFrom = edgeTo = rightSelection = null;
selection = selection === item ? null : item; workflow.states.forEach(state => {
if (selection === item) { var messages = [];
while (sideContent.firstChild) state.stateData.messages.forEach(msg => messages.push(new Message(msg)));
sideContent.removeChild(sideContent.lastChild); state.stateData.messages = messages;
sidePanel.style.display = 'block' var viewers = [];
sideHeading.innerHTML = item.name; state.stateData.viewers.forEach(v => viewers.push(new Role(v)));
var data = document.createElement('div'); state.stateData.viewers = viewers;
var content = generatePanelContent(selection); state.stateData.payload = new Payload(state.stateData.payload);
content.forEach(c => data.appendChild(c)); })
sideContent.appendChild(data);
var spStyle = window.getComputedStyle(sidePanel); workflow.actions.forEach(action => {
var shStyle = window.getComputedStyle(sideHeading); var messages = [];
sideContent.style.top = sideHeading.offsetHeight + parseFloat(spStyle.paddingTop) + parseFloat(shStyle.marginTop) + parseFloat(shStyle.marginBottom); action.actionData.messages.forEach(msg => messages.push(new Message(msg)));
var sbStyle = window.getComputedStyle(sideButtons); action.actionData.messages = messages;
sideContent.style.bottom = sideButtons.offsetHeight + parseFloat(spStyle.paddingBottom) + parseFloat(sbStyle.marginTop) + parseFloat(sbStyle.marginBottom); var viewers = [];
// console.log(sideHeading.offsetHeight + shStyle.marginTop + shStyle.marginBottom); action.actionData.viewers.forEach(v => viewers.push(new Role(v)));
action.actionData.viewers = viewers;
var actors = [];
action.actionData.actors.forEach(v => actors.push(new Role(v)));
action.actionData.actors = actors;
var viewActors = [];
action.actionData['actor Viewers'].forEach(v => viewActors.push(new Role(v)));
action.actionData['actor Viewers'] = viewActors;
action.actionData.form = new Payload(action.actionData.form);
})
workflow.actions.forEach(act => act.actionData.actors.forEach(a => {
var includes = false;
actors.forEach(actor => includes = includes || equalRoles(a, actor));
(!includes) && actors.push(a);
(!act.actionData.actorNames) && (act.actionData.actorNames = []);
act.actionData.actorNames.push(getRoleName(a));
}));
// console.log(actors);
// workflow.actions.forEach(a => console.log(a.actionData.actorNames));
function getRoleName(role) {
if (typeof role == 'string') {
return role;
} else if (role instanceof Role) {
return role.name;
} else { } else {
sidePanel.style.display = 'none'; return JSON.stringify(role);
}
console.log(item);
}
function rightSelect() {
select(rightSelection);
}
function deselect() {
sidePanel.style.display = 'none';
selection = null;
}
/**
* Updates the nodes and edges of the workflow graph.
*/
function updateGraph() {
identifyOverlappingEdges()
computeCurvatures()
Graph.graphData({nodes: workflow.states, links: workflow.actions});
}
/**
* Adds a new action between two states.
* @param {*} source The source state.
* @param {*} target The target state.
*/
function connect(source, target) {
let linkId = actionIdCounter ++;
action = {id: linkId, source: source, target: target, name: 'action_' + linkId, actionData: {
viewerNames: [], actorNames: []
}};
workflow.actions.push(action);
updateGraph();
select(action);
}
/**
* Adds a new state to the workflow and auto-selects it.
*/
function addState() {
var nodeId = stateIdCounter ++;
var x = newStateCoords.x;
var y = newStateCoords.y;
state = {id: nodeId, x: x, y: y, name: 'state_' + nodeId, fx: x, fy: y, val: 5};
workflow.states.push(state);
updateGraph();
select(state);
}
function markEdgeTo() {
edgeTo = rightSelection;
contextMenuSt.style.display = 'none';
}
function markEdgeFrom() {
edgeFrom = rightSelection;
contextMenuSt.style.display = 'none';
}
function removeSelection() {
if (selection) {
if (selection.actionData) removeAction(selection);
else removeState(selection);
deselect();
edgeFrom = edgeTo = rightSelection = null;
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
} }
} }
function removeRightSelection() { //Prepare actor highlighting
if (rightSelection) { var allActors = document.createElement('option');
if (rightSelection.actionData) removeAction(rightSelection); allActors.text = NO_ACTOR;
else removeState(rightSelection); selectedActor.add(allActors);
if (selection === rightSelection) deselect(); actors.forEach(actor => {
edgeFrom = edgeTo = rightSelection = null; var option = document.createElement('option');
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none'; option.text = getRoleName(actor);
selectedActor.add(option);
});
//Identify all viewers of every action
workflow.actions.forEach(act => {
if (act.actionData.viewers.length === 0) {
viewableByAll.push(act.actionData);
} else {
act.actionData.viewers.forEach(v => {
var includes = false;
viewers.forEach(viewer => includes = includes || equalRoles(v, viewer));
(!includes) && viewers.push(v);
(!act.actionData.viewerNames) && (act.actionData.viewerNames = []);
act.actionData.viewerNames.push(getRoleName(v));
})
} }
if (act.actionData.mode === 'initial') {
act.actionData.actorNames.forEach(an => !initiators.includes(an) && initiators.push(an));
}
});
//Identify all viewers of every state
workflow.states.forEach(st => {
if (st.name === '@@INIT') {
initState = st;
} else if (st.stateData.viewers.length === 0) {
viewableByAll.push(st.stateData);
} else {
st.stateData.viewers.forEach(v => {
var includes = false;
viewers.forEach(viewer => includes = includes || equalRoles(v, viewer));
(!includes) && viewers.push(v);
(!st.stateData.viewerNames) && (st.stateData.viewerNames = []);
st.stateData.viewerNames.push(getRoleName(v));
})
}
});
initState.stateData.viewerNames = initiators;
const ALL_VIEW = "Not explicitly specified";
if (viewableByAll.length > 0) {
viewers.push(ALL_VIEW);
var viewerNames = []
viewers.forEach(viewer => viewerNames.push(getRoleName(viewer)));
viewableByAll.forEach(data => {
data.viewerNames = viewerNames;
});
} }
//Prepare viewer highlighting
var allViewers = document.createElement('option');
allViewers.text = NO_VIEWER;
selectedViewer.add(allViewers);
viewers.forEach(viewer => {
var option = document.createElement('option');
option.text = getRoleName(viewer);
selectedViewer.add(option);
});
const edgeColourDefault = '#999999ff';
const edgeColourSelected = '#000000ff';
const edgeColourHighlightDefault = '#6ed4d4';
const edgeColourHighlightSelected = 'magenta';
const edgeColourSubtleDefault = '#99999955';
const edgeColourSubtleSelected = '#00000055';
/** /**
* Removes an edge from the workflow. * Checks if two roles are equal.
* @param {*} action The action to remove. * @param {*} role1
* @param {*} role2
* @returns
*/ */
function removeAction(action) { function equalRoles(role1, role2) {
workflow.actions.splice(workflow.actions.indexOf(action), 1); role1 instanceof Role && (role1 = role1.json);
} role2 instanceof Role && (role2 = role2.json);
var equal = role1.tag === role2.tag;
if (role1.tag == 'payload-reference') {
/** equal = equal && (role1['payload-label'] === role2['payload-label']);
* Removes a state from the workflow. } else if (role1.tag == 'user') {
* @param {*} state The state to remove. equal = equal && (role1.user === role2.user);
*/ } else if (role1.tag == 'authorized') {
function removeState(state) { equal = equal && (role1.authorized['dnf-terms'][0][0].var === role2.authorized['dnf-terms'][0][0].var);
workflow.actions }
.filter(edge => edge.source === state || edge.target === state) return equal;
.forEach(edge => removeAction(edge));
workflow.states.splice(workflow.states.indexOf(state), 1);
var abbreviation = state.stateData && state.stateData.abbreviation;
abbreviation && stateAbbreviations.splice(stateAbbreviations.indexOf(abbreviation), 1);
} }
@ -531,7 +531,6 @@ function getEdgeColour(edge) {
} }
//Compute abbreviations of the names of all states //Compute abbreviations of the names of all states
var stateAbbreviations = [];
workflow.states.forEach(state => { workflow.states.forEach(state => {
// var label = node.name.substring(0, 5); // var label = node.name.substring(0, 5);
var label = state.name.split(' '); // [node.name.substring(0, 6), node.name.substring(6, 12), node.name.substring(12, 18)]; var label = state.name.split(' '); // [node.name.substring(0, 6), node.name.substring(6, 12), node.name.substring(12, 18)];
@ -563,10 +562,7 @@ function openContextMenu(x, y, menu) {
edgeFrom = edgeTo = null; edgeFrom = edgeTo = null;
} }
var newStateCoords = {'x': 0, 'y': 0}; //Initial coordinates of the next new state wfGraph
const Graph = ForceGraph()
(document.getElementById('graph'))
.linkDirectionalArrowLength(6) .linkDirectionalArrowLength(6)
.linkDirectionalArrowRelPos(1) .linkDirectionalArrowRelPos(1)
.linkColor(getEdgeColour) .linkColor(getEdgeColour)
@ -574,7 +570,7 @@ const Graph = ForceGraph()
.linkCanvasObjectMode(() => 'after') .linkCanvasObjectMode(() => 'after')
.linkCanvasObject((edge, context) => { .linkCanvasObject((edge, context) => {
const MAX_FONT_SIZE = 4; const MAX_FONT_SIZE = 4;
const LABEL_NODE_MARGIN = Graph.nodeRelSize() * edge.source.val * 1.5; const LABEL_NODE_MARGIN = wfGraph.nodeRelSize() * edge.source.val * 1.5;
const source = edge.source; const source = edge.source;
const target = edge.target; const target = edge.target;
@ -716,7 +712,7 @@ const Graph = ForceGraph()
closeMenuItem(); closeMenuItem();
}) })
.onBackgroundRightClick(event => { .onBackgroundRightClick(event => {
newStateCoords = Graph.screen2GraphCoords(event.layerX, event.layerY); newStateCoords = wfGraph.screen2GraphCoords(event.layerX, event.layerY);
openContextMenu(event.layerX, event.layerY, contextMenuBg); openContextMenu(event.layerX, event.layerY, contextMenuBg);
contextMenuEd.style.display = contextMenuSt.style.display = 'none'; contextMenuEd.style.display = contextMenuSt.style.display = 'none';
edgeFrom = edgeTo = rightSelection = null; edgeFrom = edgeTo = rightSelection = null;
@ -725,3 +721,4 @@ const Graph = ForceGraph()
.autoPauseRedraw(false); .autoPauseRedraw(false);
updateGraph(); updateGraph();
}