added context menus with quick actions

This commit is contained in:
David Mosbach 2023-05-27 23:14:57 +02:00
parent 4dbe66ab5f
commit 57b6aeacdf
3 changed files with 180 additions and 23 deletions

View File

@ -76,4 +76,41 @@ body {
hyphens: auto; hyphens: auto;
word-wrap: break-word; word-wrap: break-word;
height: 80%; height: 80%;
}
.contextmenu {
display: none;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
border-radius: 5px;
line-height: 2;
position: absolute;
z-index: 15;
}
.contextmenu div {
padding: 5px;
background-color: rgb(230, 230, 230);
opacity: 0.95;
transition: background-color 100ms ease-out 50ms;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.contextmenu .menutop {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.contextmenu .menubottom {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
.contextmenu div:hover {
background-color: rgb(240, 240, 240);
}
.contextmenu div:active {
background-color: rgb(250, 250, 250);
} }

View File

@ -35,7 +35,64 @@
</svg> </svg>
</div> </div>
<div id="sidecontent"></div> <div id="sidecontent"></div>
</div>
<div id="ctmenubg" class="contextmenu">
<div class="menutop" onclick="addState()">
<svg height="10" width="10" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="5" r="4" stroke="rgb(63, 63, 63)" stroke-width="2" fill="none" />
</svg>
&nbsp;New State
</div>
<div class="menubottom">
<svg height="10" width="10" xmlns="http://www.w3.org/2000/svg">
<polyline points="1,1 9,9" style="fill:none;stroke:rgb(63, 63, 63);stroke-width:2" stroke-linecap="round" />
</svg>
&nbsp;New Edge
</div>
</div>
<div id="ctmenust" class="contextmenu">
<div class="menutop">
<svg height="10" width="10" xmlns="http://www.w3.org/2000/svg">
<polyline points="1,1 9,9" style="fill:none;stroke:rgb(63, 63, 63);stroke-width:2" stroke-linecap="round" />
</svg>
&nbsp;New Edge from here
</div>
<div class="menucenter">
<svg height="10" width="10" xmlns="http://www.w3.org/2000/svg">
<polyline points="1,9 9,1" style="fill:none;stroke:rgb(63, 63, 63);stroke-width:2" stroke-linecap="round" />
</svg>
&nbsp;New Edge to here
</div>
<div class="menucenter" onclick="rightSelect()">
<svg height="10" width="10" xmlns="http://www.w3.org/2000/svg">
<polyline points="1,9 9,1" style="fill:none;stroke:rgb(63, 63, 63);stroke-width:4" />
<polyline points="1,9 8,2" style="fill:none;stroke:rgb(63, 63, 63);stroke-width:2" stroke-linecap="round" />
</svg>
&nbsp;Edit
</div>
<div class="menubottom" onclick="removeRightSelection()">
<svg height="10" width="10" xmlns="http://www.w3.org/2000/svg">
<polyline points="1,1 9,9" style="fill:none;stroke:rgb(183, 76, 76);stroke-width:2" stroke-linecap="round" />
<polyline points="9,1 1,9" style="fill:none;stroke:rgb(183, 76, 76);stroke-width:2" stroke-linecap="round" />
</svg>
&nbsp;Delete
</div>
</div>
<div id="ctmenued" class="contextmenu">
<div class="menutop" onclick="rightSelect()">
<svg height="10" width="10" xmlns="http://www.w3.org/2000/svg">
<polyline points="1,9 9,1" style="fill:none;stroke:rgb(63, 63, 63);stroke-width:4" />
<polyline points="1,9 8,2" style="fill:none;stroke:rgb(63, 63, 63);stroke-width:2" stroke-linecap="round" />
</svg>
&nbsp;Edit
</div>
<div class="menubottom" onclick="removeRightSelection()">
<svg height="10" width="10" xmlns="http://www.w3.org/2000/svg">
<polyline points="1,1 9,9" style="fill:none;stroke:rgb(183, 76, 76);stroke-width:2" stroke-linecap="round" />
<polyline points="9,1 1,9" style="fill:none;stroke:rgb(183, 76, 76);stroke-width:2" stroke-linecap="round" />
</svg>
&nbsp;Delete
</div>
</div> </div>
<div id="graph"></div> <div id="graph"></div>
</div> </div>

107
editor.js
View File

@ -123,7 +123,8 @@ var highlightedSources = [];
var highlightedTargets = []; var highlightedTargets = [];
function selectActor() { function selectActor() {
// console.log(selectedActor.value); contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
rightSelection = null;
highlightedSources = []; highlightedSources = [];
highlightedTargets = []; highlightedTargets = [];
selectedViewer.value = NO_VIEWER; selectedViewer.value = NO_VIEWER;
@ -136,6 +137,8 @@ function selectActor() {
} }
function selectViewer() { function selectViewer() {
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
rightSelection = null;
highlightedSources = []; highlightedSources = [];
highlightedTargets = []; highlightedTargets = [];
selectedActor.value = NO_ACTOR; selectedActor.value = NO_ACTOR;
@ -153,8 +156,13 @@ const selfLoopCurvMin = 0.5; // Minimum curvature of a self loop.
const curvatureMinMax = 0.2; // Minimum/maximum curvature (1 +/- x) of overlapping edges. 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.
const sidePanel = document.getElementById('sidepanel'); const sidePanel = document.getElementById('sidepanel');
const sideContent = document.getElementById('sidecontent'); const sideContent = document.getElementById('sidecontent');
//Context menus
const contextMenuBg = document.getElementById('ctmenubg'); //Click on background
const contextMenuSt = document.getElementById('ctmenust'); //Click on state
const contextMenuEd = document.getElementById('ctmenued'); //Click on edge
const edgeColourDefault = '#999999ff'; const edgeColourDefault = '#999999ff';
const edgeColourSelected = '#000000ff'; const edgeColourSelected = '#000000ff';
@ -254,6 +262,8 @@ function generatePanelContent(selection) {
* @param {*} item The node or edge to select. * @param {*} item The node or edge to select.
*/ */
function select(item) { function select(item) {
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
rightSelection = null;
selection = selection === item ? null : item; selection = selection === item ? null : item;
if (selection === item) { if (selection === item) {
while (sideContent.firstChild) while (sideContent.firstChild)
@ -268,7 +278,11 @@ function select(item) {
sidePanel.style.display = 'none'; sidePanel.style.display = 'none';
} }
console.log(item); console.log(item);
// TODO }
function rightSelect() {
select(rightSelection);
} }
@ -296,17 +310,33 @@ function connect(source, target) {
/** /**
* Adds a new state to the workflow. * Adds a new state to the workflow and auto-selects it.
* @param {*} x The x coordinate on the canvas.
* @param {*} y The y coordinate on the canvas.
* @returns The new state.
*/ */
function addState(x, y) { function addState() {
let nodeId = stateIdCounter ++; 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}; state = {id: nodeId, x: x, y: y, name: 'state_' + nodeId, fx: x, fy: y, val: 5};
workflow.states.push(state); workflow.states.push(state);
updateGraph(); updateGraph();
return state; select(state);
}
function addEdge() {
//TODO
}
function removeRightSelection() {
if (rightSelection) {
if (rightSelection.actionData) removeAction(rightSelection);
else removeState(rightSelection);
if (selection === rightSelection) {
selection = null;
sidePanel.style.display = 'none';
}
rightSelection = null;
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
}
} }
@ -328,6 +358,8 @@ function removeState(state) {
.filter(edge => edge.source === state || edge.target === state) .filter(edge => edge.source === state || edge.target === state)
.forEach(edge => removeAction(edge)); .forEach(edge => removeAction(edge));
workflow.states.splice(workflow.states.indexOf(state), 1); workflow.states.splice(workflow.states.indexOf(state), 1);
var abbreviation = state.stateData && state.stateData.abbreviation;
abbreviation && stateAbbreviations.splice(stateAbbreviations.indexOf(abbreviation), 1);
} }
@ -340,18 +372,19 @@ function getNodeColour(node) {
var standard = (selectedActor.value === NO_ACTOR && selectedViewer.value === NO_VIEWER) var standard = (selectedActor.value === NO_ACTOR && selectedViewer.value === NO_VIEWER)
|| highlightedSources.includes(node.id) || highlightedTargets.includes(node.id) || highlightedSources.includes(node.id) || highlightedTargets.includes(node.id)
var alpha = standard ? 'ff' : '55'; var alpha = standard ? 'ff' : '55';
var isSelected = selection === node || rightSelection === node;
if (node.stateData && node.stateData.final !== 'False' && node.stateData.final !== '') { if (node.stateData && node.stateData.final !== 'False' && node.stateData.final !== '') {
if (node.stateData.final === 'True' || node.stateData.final === 'ok') { if (node.stateData.final === 'True' || node.stateData.final === 'ok') {
return (selection === node ? '#a4eb34' : '#7fad36') + alpha; return (isSelected ? '#a4eb34' : '#7fad36') + alpha;
} else if (node.stateData.final === 'not-ok') { } else if (node.stateData.final === 'not-ok') {
return (selection === node ? '#f77474' : '#f25050') + alpha; return (isSelected ? '#f77474' : '#f25050') + alpha;
} else { } else {
//console.log(node.stateData.final); //console.log(node.stateData.final);
} }
} else if (node.name === '@@INIT') { } else if (node.name === '@@INIT') {
return (selection === node ? '#e8cd84' : '#d1ad4b') + alpha; return (isSelected ? '#e8cd84' : '#d1ad4b') + alpha;
} else { } else {
return (selection === node ? '#5fbad9' : '#4496b3') + alpha; return (isSelected ? '#5fbad9' : '#4496b3') + alpha;
} }
} }
@ -364,12 +397,13 @@ function isHighlightedEdge(edge) {
} }
function getEdgeColour(edge) { function getEdgeColour(edge) {
var isSelected = selection === edge || rightSelection === edge;
if (isHighlightedEdge(edge)) { if (isHighlightedEdge(edge)) {
return selection === edge ? edgeColourHighlightSelected : edgeColourHighlightDefault; return isSelected ? edgeColourHighlightSelected : edgeColourHighlightDefault;
} else if (selectedActor.value !== NO_ACTOR) { } else if (selectedActor.value !== NO_ACTOR) {
return selection === edge ? edgeColourSubtleSelected : edgeColourSubtleDefault; return isSelected ? edgeColourSubtleSelected : edgeColourSubtleDefault;
} else { } else {
return selection === edge ? edgeColourSelected : edgeColourDefault; return isSelected ? edgeColourSelected : edgeColourDefault;
} }
} }
@ -393,6 +427,20 @@ workflow.states.forEach(state => {
state.stateData.abbreviation = labelString; state.stateData.abbreviation = labelString;
}); });
/**
*
* @param {*} event
* @param {HTMLElement} menu
*/
function openContextMenu(x, y, menu) {
menu.style.top = y - 25;
menu.style.left = x + 20;
menu.style.display = 'block';
}
var newStateCoords = {'x': 0, 'y': 0}; //Initial coordinates of the next new state
const Graph = ForceGraph() const Graph = ForceGraph()
(document.getElementById('graph')) (document.getElementById('graph'))
.linkDirectionalArrowLength(6) .linkDirectionalArrowLength(6)
@ -505,13 +553,28 @@ const Graph = ForceGraph()
node.fy = node.y; node.fy = node.y;
}) })
.onNodeClick((node, _) => select(node)) .onNodeClick((node, _) => select(node))
.onNodeRightClick((node, _) => removeState(node)) .onNodeRightClick((node, event) => {
openContextMenu(event.layerX, event.layerY, contextMenuSt);
contextMenuBg.style.display = contextMenuEd.style.display = 'none';
rightSelection = node;
})
.onLinkClick((edge, _) => select(edge)) .onLinkClick((edge, _) => select(edge))
.onLinkRightClick((edge, _) => removeAction(edge)) .onLinkRightClick((edge, event) => {
.onBackgroundClick(event => { openContextMenu(event.layerX, event.layerY, contextMenuEd);
var coords = Graph.screen2GraphCoords(event.layerX, event.layerY); contextMenuBg.style.display = contextMenuSt.style.display = 'none';
var newState = addState(coords.x, coords.y); rightSelection = edge;
selection = newState; })
.onBackgroundClick(_ => {
contextMenuEd.style.display = contextMenuSt.style.display = contextMenuBg.style.display = 'none';
sidePanel.style.display = 'none';
rightSelection = null;
selection = null;
})
.onBackgroundRightClick(event => {
newStateCoords = Graph.screen2GraphCoords(event.layerX, event.layerY);
openContextMenu(event.layerX, event.layerY, contextMenuBg);
contextMenuEd.style.display = contextMenuSt.style.display = 'none';
rightSelection = null;
}) })
.autoPauseRedraw(false); .autoPauseRedraw(false);