+
-
File
New Workflow
- Open
+ Open
Save
Save As
Export
+
-
Edit
Undo
@@ -33,7 +33,7 @@
Run Linter
+
@@ -42,7 +42,7 @@
-
View
| - + |
| - + |
+
@@ -69,7 +69,7 @@
Settings
|
@@ -121,14 +121,14 @@
-
+ About
Visualiser & editor for Uni2work workflows
+
-
-
+
+
Hello-
-
-
-
-
+
+
+
+
Hello-
-
+
-
+
-
+
+
+
+
-
+
+ ' + workflowFiles[i].name + '' + workflowFiles[i].description; - var url = 'http://localhost:8080' + workflowFiles[i].url; - defineOnClick(item, url, workflowFiles[i].name); - fileContent.appendChild(item); + var item = document.createElement('div'); + item.innerHTML = '' + workflowFiles[i].name + '' + workflowFiles[i].description; + var url = 'http://localhost:8080/spaß' + workflowFiles[i].url; + defineOnClick(item, url, workflowFiles[i].name); + fileContent.appendChild(item); } - var url = 'http://localhost:8080' + workflowFiles[0].url; + var url = 'http://localhost:8080/spaß' + workflowFiles[0].url; return fetch(url); - }) - .then((response) => response.json()) - .then((data) => { +}) + .then((response) => response.json()) + .then((data) => { document.getElementById('filename').innerText = workflowFiles[0].name; document.title = workflowFiles[0].name + ' | Editor'; - for (var key in data) - workflow[key] = data[key]; - wfGraph(document.getElementById('graph')).graphData({nodes: workflow.states, links: workflow.actions}); - Array.from(document.getElementsByClassName('graph-tooltip')).forEach(tooltip => - tooltip.classList.add('menu-lightmode') - ); + workflow = new WF.Workflow({ + states: data.states, + actions: data.actions + }); + wfGraph(document.getElementById('graph')).graphData({ nodes: workflow.states, links: workflow.actions }); + Array.from(document.getElementsByClassName('graph-tooltip')).forEach(tooltip => tooltip.classList.add('menu-lightmode')); runnn(); - }); - +}); //Actors of the workflow var actors = []; const selectedActor = document.getElementById('actor'); @@ -344,45 +344,41 @@ const selectedActor = document.getElementById('actor'); var viewers = []; const selectedViewer = document.getElementById('viewer'); //Actions/States with no explicit viewers -var viewableByAll = [] +var viewableByAll = []; //Possible initiators -var initiators = [] +var initiators = []; //Implicit state from which initial actions can be selected var initState = null; const NO_ACTOR = 'None'; const NO_VIEWER = NO_ACTOR; - //source & target nodes of all currently highlighted actions var highlightedSources = []; var highlightedTargets = []; - -function selectActor() { - closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); - edgeFrom = edgeTo = rightSelection = null; - highlightedSources = []; - highlightedTargets = []; - selectedViewer.value = NO_VIEWER; - workflow.actions.forEach(act => { - if (act.actionData.mode != 'automatic' && act.actionData.actorNames.includes(selectedActor.value)) { - highlightedSources.push(act.source.id); - highlightedTargets.push(act.target.id); - } - }); +export function selectActor() { + closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); + edgeFrom = edgeTo = rightSelection = null; + highlightedSources = []; + highlightedTargets = []; + selectedViewer.value = NO_VIEWER; + workflow.actions.forEach(act => { + if (act.actionData.mode != 'automatic' && act.actionData.actorNames.includes(selectedActor.value)) { + highlightedSources.push(act.source.id); + highlightedTargets.push(act.target.id); + } + }); } - -function selectViewer() { - closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); - edgeFrom = edgeTo = rightSelection = null; - highlightedSources = []; - highlightedTargets = []; - selectedActor.value = NO_ACTOR; - workflow.states.forEach(st => { - if (st.stateData.viewerNames.includes(selectedViewer.value)) { - highlightedSources.push(st.id); - } - }); +export function selectViewer() { + closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); + edgeFrom = edgeTo = rightSelection = null; + highlightedSources = []; + highlightedTargets = []; + selectedActor.value = NO_ACTOR; + workflow.states.forEach(st => { + if (st.stateData.viewerNames.includes(selectedViewer.value)) { + highlightedSources.push(st.id); + } + }); } - var selection = null; // The currently selected node/edge. var rightSelection = null; // The currently right clicked node/edge. var edgeTo = null; // Target of an edge to be created. @@ -391,683 +387,722 @@ var edgeFrom = null; // Start on an edge to be created. const curtain = document.getElementById('curtain'); const submenuBackdrop = document.getElementById('submenu-backdrop'); //Side Panel -const sidePanel = document.getElementById('sidepanel'); -const sideContent = document.getElementById('sidecontent'); -const sideHeading = document.getElementById('sideheading'); -const sideButtons = document.getElementById('sidebuttons'); +const sidePanel = document.getElementById('sidepanel'); +const sideContent = document.getElementById('sidecontent'); +const sideHeading = document.getElementById('sideheading'); +const sideButtons = document.getElementById('sidebuttons'); const sideInfoEdge = document.getElementById('sidecontentedge'); const sideInfoNode = document.getElementById('sidecontentnode'); //File panel -const fileMenu = document.getElementById('filemenu'); -const filePanel = document.getElementById('filepanel'); +const fileMenuBtn = document.getElementById('file-menu-btn'); +const fileMenu = document.getElementById('filemenu'); +const filePanel = document.getElementById('filepanel'); const fileHeading = document.getElementById('fileheading'); const fileContent = document.getElementById('filecontent'); const fileButtons = document.getElementById('filebuttons'); +//Edit +const editMenuBtn = document.getElementById('edit-menu-btn'); +//View +const viewMenuBtn = document.getElementById('view-menu-btn'); +//Settings +const settingsMenuBtn = document.getElementById('settings-menu-btn'); +//About +const aboutMenuBtn = document.getElementById('about-menu-btn'); //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 //Search -const searchContainer = document.getElementById('search-container'); -const searchInput = document.getElementById('search-input'); -const searchResults = document.getElementById('search-results'); +const searchMenuBtn = document.getElementById('search-menu-btn'); +const searchContainer = document.getElementById('search-container'); +const searchInput = document.getElementById('search-input'); +const searchResults = document.getElementById('search-results'); const searchResultList = document.getElementById('search-result-list'); -const searchOptions = document.getElementById('search-options') +const searchOptions = document.getElementById('search-options'); // Counters for placeholder IDs of states/actions added via GUI -var stateIdCounter = 0; +var stateIdCounter = 0; var actionIdCounter = 0; var stateAbbreviations = []; -var newStateCoords = {'x': 0, 'y': 0}; //Initial coordinates of the next new state - -sidePanel.style.top = mainMenu.offsetHeight + 15; -searchContainer.style.left = mainMenu.offsetWidth / 2 - searchContainer.offsetWidth / 2; +var newStateCoords = { 'x': 0, 'y': 0 }; //Initial coordinates of the next new state +sidePanel.style.top = (mainMenu.offsetHeight + 15).toString(); +searchContainer.style.left = (mainMenu.offsetWidth / 2 - searchContainer.offsetWidth / 2).toString(); searchResults.style.left = searchContainer.style.left; -searchResults.style.maxHeight = 0.8 * window.innerHeight; - - +searchResults.style.maxHeight = (0.8 * window.innerHeight).toString(); +//Event handlers +curtain.addEventListener('click', _ => closeFileDisplay()); +fileMenuBtn.addEventListener('click', function (_) { openFileMenu(this); }); +editMenuBtn.addEventListener('click', function (_) { openEditMenu(this); }); +viewMenuBtn.addEventListener('click', function (_) { openViewMenu(this); }); +settingsMenuBtn.addEventListener('click', function (_) { openSettingsMenu(this); }); +aboutMenuBtn.addEventListener('click', function (_) { openAboutMenu(this); }); +searchMenuBtn.addEventListener('click', function (_) { openSearchMenu(this); }); +searchInput.addEventListener('click', function (_) { showSearchResults(); }); +searchInput.addEventListener('input', function (_) { search(this.value); }); +document.getElementById('search-button')?.addEventListener('click', _ => searchInput.focus()); +document.getElementById('open-file')?.addEventListener('click', _ => openFileDisplay()); +document.getElementById('actor')?.addEventListener('change', _ => selectActor()); +document.getElementById('viewer')?.addEventListener('change', _ => selectViewer()); +document.getElementById('theme-toggle')?.addEventListener('click', _ => toggleTheme()); +document.getElementById('side-panel-cancel')?.addEventListener('click', _ => deselect()); +document.getElementById('side-panel-focus')?.addEventListener('click', _ => focusSelection()); +document.getElementById('side-panel-delete')?.addEventListener('click', _ => removeSelection()); +document.getElementById('file-panel-cancel')?.addEventListener('click', _ => closeFileDisplay()); +document.getElementById('add-state')?.addEventListener('click', _ => addState()); +document.getElementById('edge-from')?.addEventListener('click', _ => markEdgeFrom()); +document.getElementById('edge-to')?.addEventListener('click', _ => markEdgeTo()); +document.getElementById('close-side-panel')?.addEventListener('click', _ => deselect()); +document.getElementById('close-file-panel')?.addEventListener('click', _ => closeFileDisplay()); +document.querySelectorAll('.edit-item').forEach(elem => elem.addEventListener('click', _ => rightSelect())); +document.querySelectorAll('.delete-item').forEach(elem => elem.addEventListener('click', _ => removeRightSelection())); /** * Marks the given item as selected. * @param {*} item The node or edge to select. */ -function select(item) { - closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); - edgeFrom = edgeTo = rightSelection = null; - selection = selection === item ? null : item; - if (selection === item) { - while (sideContent.firstChild) - sideContent.removeChild(sideContent.lastChild); - function callback() { - if (item.actionData) { - sideInfoEdge.style.display = 'block'; - sideInfoNode.style.display = 'none'; - } else { - sideInfoEdge.style.display = 'none'; - sideInfoNode.style.display = 'block'; - } - var heading = item.name; - if (heading.length > 90) heading = heading.substring(0, 88) + '...'; - sideHeading.innerHTML = heading; - sideHeading.setAttribute('title', 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); +export function select(item) { + closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); + edgeFrom = edgeTo = rightSelection = null; + selection = selection === item ? null : item; + if (selection === item) { + while (sideContent.firstChild) + sideContent.removeChild(sideContent.lastChild); + function callback() { + if (item.hasOwnProperty('actionData')) { + sideInfoEdge.style.display = 'block'; + sideInfoNode.style.display = 'none'; + } + else { + sideInfoEdge.style.display = 'none'; + sideInfoNode.style.display = 'block'; + } + var heading = item.name; + if (heading.length > 90) + heading = heading.substring(0, 88) + '...'; + sideHeading.innerHTML = heading; + sideHeading.setAttribute('title', 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)).toString(); + var sbStyle = window.getComputedStyle(sideButtons); + sideContent.style.bottom = (sideButtons.offsetHeight + parseFloat(spStyle.paddingBottom) + parseFloat(sbStyle.marginTop) + parseFloat(sbStyle.marginBottom)).toString(); + // console.log(sideHeading.offsetHeight + shStyle.marginTop + shStyle.marginBottom); + } + fadeIn(callback, { element: sidePanel, max: 1 }); } - fadeIn(callback, {element: sidePanel, max: 1}); - - } else { - fadeOut(null, {element: sidePanel, min: 0}); + else { + fadeOut(null, { element: sidePanel, min: 0 }); + // sidePanel.style.display = 'none'; + } + console.log(item); +} +export function deselect() { + fadeOut(null, { element: sidePanel, min: 0 }); // sidePanel.style.display = 'none'; - } - console.log(item); + selection = null; } - -function deselect() { - fadeOut(null, {element: sidePanel, min: 0}); - // sidePanel.style.display = 'none'; - selection = null; +export function rightSelect() { + if (!rightSelection) + return; + select(rightSelection); } - - -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: 'state_' + nodeId, x: x, y: y, name: 'state_' + nodeId, fx: x, fy: y, val: 5}; - workflow.states.push(state); - updateGraph(); - select(state); - nodeIndex.add(state.id, state.name); +export function addState() { + var nodeId = stateIdCounter++; + var x = newStateCoords.x; + var y = newStateCoords.y; + var state = { id: 'state_' + nodeId, + x: x, + y: y, + name: 'state_' + nodeId, + fx: x, + fy: y, + val: 5, + stateData: new WF.StateData({ abbreviation: `S${nodeId}`, final: 'false' }) }; + workflow.states.push(state); + updateGraph(); + select(state); + nodeIndex.add(state.id, state.name); } - /** * Adds a new action between two states. - * @param {*} source The source state. - * @param {*} target The target state. + * @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); - actionIndex.add(action.id, action.name); + let linkId = actionIdCounter++; + var action = new WF.WFEdge({ + id: (linkId).toString(), + source: source, + target: target, + name: 'action_' + linkId, + actionData: {}, + nodePairId: '' + }); + workflow.actions.push(action); + updateGraph(); + select(action); + actionIndex.add(action.id, action.name); } - -function markEdgeTo() { - edgeTo = rightSelection; - closeContextMenus(contextMenuSt); - // contextMenuSt.style.display = 'none'; +export function markEdgeTo() { + edgeTo = rightSelection; + closeContextMenus(contextMenuSt); + // contextMenuSt.style.display = 'none'; } - -function markEdgeFrom() { - edgeFrom = rightSelection; - closeContextMenus(contextMenuSt); - // contextMenuSt.style.display = 'none'; +export function markEdgeFrom() { + edgeFrom = rightSelection; + closeContextMenus(contextMenuSt); + // contextMenuSt.style.display = 'none'; } - -function removeSelection() { - if (selection) { - if (selection.actionData) removeAction(selection); - else removeState(selection); - deselect(); - edgeFrom = edgeTo = rightSelection = null; - closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); - } -} - -function removeRightSelection() { - if (rightSelection) { - if (rightSelection.actionData) removeAction(rightSelection); - else removeState(rightSelection); - if (selection === rightSelection) deselect(); - edgeFrom = edgeTo = rightSelection = null; - closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); - } -} - -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); +export function removeSelection() { + if (selection) { + if (selection.hasOwnProperty('actionData')) + removeAction(selection); + else + removeState(selection); + deselect(); + edgeFrom = edgeTo = rightSelection = null; + closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); } - - } - return children; } - +export function removeRightSelection() { + if (rightSelection) { + if (rightSelection.hasOwnProperty('actionData')) + removeAction(rightSelection); + else + removeState(rightSelection); + if (selection === rightSelection) + deselect(); + edgeFrom = edgeTo = rightSelection = null; + closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); + } +} +function generatePanelContent(selection) { + var children = []; + var data = selection.hasOwnProperty('stateData') ? 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 WF.Message) { + content.forEach(msg => { + if (msg instanceof WF.Message) + msg.format().forEach(child => children.push(child)); + else { + var m = document.createElement('p'); + m.innerHTML = msg; + children.push(m); + } + }); + } + else if (content instanceof WF.Payload) { + content.format().forEach(child => children.push(child)); + } + else if (content instanceof WF.Roles) { + content.format().forEach(child => children.push(child)); + } + else { + var p = document.createElement('p'); + var text = document.createTextNode((key == 'comment') + ? data[key].join(' ') + : JSON.stringify(data[key])); + p.appendChild(text); + children.push(p); + } + } + return children; +} /** * Removes an edge from the workflow. - * @param {*} action The action to remove. + * @param action The action to remove. */ function removeAction(action) { - workflow.actions.splice(workflow.actions.indexOf(action), 1); - actionIndex.remove(action.id); + workflow.actions.splice(workflow.actions.indexOf(action), 1); + actionIndex.remove(action.id); } - - /** * Removes a state from the workflow. * @param {*} state The state to remove. */ function removeState(state) { - workflow.actions - .filter(edge => edge.source === state || edge.target === state) - .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); - nodeIndex.remove(state.id); + workflow.actions + .filter(edge => edge.source === state || edge.target === state) + .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); + nodeIndex.remove(state.id); } - -var selfLoops = {}; // All edges whose targets equal their sources. -var overlappingEdges = {}; // All edges whose target and source are connected by further. +var selfLoops = new Map(); // All edges whose targets equal their sources. +var overlappingEdges = new Map(); // 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. - /** * Updates the nodes and edges of the workflow graph. */ function updateGraph() { - identifyOverlappingEdges() - computeCurvatures() - wfGraph.graphData({nodes: workflow.states, links: workflow.actions}); + identifyOverlappingEdges(); + computeCurvatures(); + wfGraph.graphData({ nodes: workflow.states, links: workflow.actions }); } - /** * Identifies and stores self loops as well as overlapping edges (i.e. multiple edges sharing the * same source and target). */ function identifyOverlappingEdges() { - selfLoops = {}; - overlappingEdges = {}; - workflow.actions.forEach(edge => { - var source = typeof(edge.source) === 'string' ? edge.source : edge.source.id; - var target = typeof(edge.target) === 'string' ? edge.target : edge.target.id; - var pre = source <= target ? source : target; - var post = source <= target ? target : source; - edge.nodePairId = pre + '_' + post; - var category = edge.source === edge.target ? selfLoops : overlappingEdges; - if (!category[edge.nodePairId]) category[edge.nodePairId] = []; - category[edge.nodePairId].push(edge); - }); + selfLoops = new Map(); + overlappingEdges = new Map(); + workflow.actions.forEach(edge => { + var source = typeof (edge.source) === 'string' ? edge.source : edge.source.id; + var target = typeof (edge.target) === 'string' ? edge.target : edge.target.id; + var pre = source <= target ? source : target; + var post = source <= target ? target : source; + edge.nodePairId = pre + '_' + post; + var category = edge.source === edge.target ? selfLoops : overlappingEdges; + if (!category.has(edge.nodePairId)) + category.set(edge.nodePairId, []); + category.get(edge.nodePairId).push(edge); + }); } - - /** * Computes the curvature of the loops stored in `selfLoops` and overlapping edges * stored in `overlappingEdges`. */ function computeCurvatures() { - // Self loops - Object.keys(selfLoops).forEach(id => { - var edges = selfLoops[id]; - for (let i = 0; i < edges.length; i++) - edges[i].curvature = selfLoopCurvMin + i / 10; - }); - // Overlapping edges - Object.keys(overlappingEdges) - .filter(nodePairId => overlappingEdges[nodePairId].length > 1) - .forEach(nodePairId => { - var edges = overlappingEdges[nodePairId]; - var lastIndex = edges.length - 1; - var lastEdge = edges[lastIndex]; - lastEdge.curvature = curvatureMinMax; - let delta = 2 * curvatureMinMax / lastIndex; - for (let i = 0; i < lastIndex; i++) { - edges[i].curvature = - curvatureMinMax + i * delta; - if (lastEdge.source !== edges[i].source) edges[i].curvature *= -1; - } - }); -} - - -function prepareWorkflow() { - actors = []; - viewers = []; - viewableByAll = [] - initiators = [] - highlightedSources = []; - highlightedTargets = []; - stateAbbreviations = []; - stateIdCounter = workflow.states ? workflow.states.length : 0; - actionIdCounter = workflow.states ? workflow.actions.length : 0; - Array.from(selectedActor.options).forEach(option => selectedActor.remove(option)); - Array.from(selectedViewer.options).forEach(option => selectedViewer.remove(option)); - selectedActor.value = NO_ACTOR; - selectedViewer.value = NO_VIEWER; - - //Parse workflow & create search index - - 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); - nodeIndex.add(state.id, state.name); - }) - - 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); - actionIndex.add(action.id, action.name); - }) - - 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)); - })); - - //Prepare actor highlighting - 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); - }); - - //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; + // Self loops + Array.from(selfLoops.keys()).forEach(id => { + var edges = selfLoops.get(id); + if (!edges) { + console.error('Undefined nodePairId: ' + id); + return; + } + for (let i = 0; i < edges.length; i++) + edges[i].curvature = selfLoopCurvMin + i / 10; + }); + // Overlapping edges + Array.from(overlappingEdges.keys()) + .filter(nodePairId => overlappingEdges.get(nodePairId).length > 1) + .forEach(nodePairId => { + var edges = overlappingEdges.get(nodePairId); + var lastIndex = edges.length - 1; + var lastEdge = edges[lastIndex]; + lastEdge.curvature = curvatureMinMax; + let delta = 2 * curvatureMinMax / lastIndex; + for (let i = 0; i < lastIndex; i++) { + edges[i].curvature = -curvatureMinMax + i * delta; + if (lastEdge.source !== edges[i].source) + edges[i].curvature *= -1; + } + }); +} +function prepareWorkflow() { + actors = []; + viewers = []; + viewableByAll = []; + initiators = []; + highlightedSources = []; + highlightedTargets = []; + stateAbbreviations = []; + stateIdCounter = workflow.states ? workflow.states.length : 0; + actionIdCounter = workflow.states ? workflow.actions.length : 0; + Array.from(selectedActor.options).forEach(option => selectedActor.remove(parseFloat(option.value))); + Array.from(selectedViewer.options).forEach(option => selectedViewer.remove(parseFloat(option.value))); + selectedActor.value = NO_ACTOR; + selectedViewer.value = NO_VIEWER; + //Create search index + workflow.states.forEach(state => nodeIndex.add(state.id, state.name)); + workflow.actions.forEach(action => actionIndex.add(action.id, action.name)); + workflow.actions.forEach(act => act.actionData.actors.roles.forEach(a => { + var includes = false; + actors.forEach(actor => includes = includes || equalRoles(a, actor)); + (!includes) && actors.push(a); + act.actionData.actorNames.push(getRoleName(a)); + })); + //Prepare actor highlighting + 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); + }); + //Identify all viewers of every action + workflow.actions.forEach(act => { + if (act.actionData.viewers.roles.length === 0) { + viewableByAll.push(act.actionData); + } + else { + act.actionData.viewers.roles.forEach(v => { + var includes = false; + viewers.forEach(viewer => includes = includes || equalRoles(v, viewer)); + (!includes) && viewers.push(v); + 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.roles.forEach(v => { + var includes = false; + viewers.forEach(viewer => includes = includes || equalRoles(v, viewer)); + (!includes) && viewers.push(v); + st.stateData.viewerNames.push(getRoleName(v)); + }); + } + }); + if (initState) + initState.stateData.viewerNames = initiators; + else + console.error('Failed to determine initial state'); + 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); + }); + //Compute abbreviations of the names of all states + workflow.states.forEach(state => { + // 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)]; + for (var i = 0; i < label.length; i++) { + if (label[i] === '(') + continue; // if the state name contains whitespace after the brace + var isBrace = label[i][0] === '('; + label[i] = label[i].substring(isBrace ? 1 : 0, isBrace ? 2 : 1); + } + var labelString = label.join('').substring(0, 6); + var counter = 1; + var len = labelString.length; + while (stateAbbreviations.includes(labelString)) { + labelString = labelString.substring(0, len) + "'" + counter++; + } + stateAbbreviations.push(labelString); + state.stateData.abbreviation = labelString; }); - } - - //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); - }); - - //Compute abbreviations of the names of all states - workflow.states.forEach(state => { - // 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)]; - for (var i = 0; i < label.length; i++) { - if (label[i] === '(') continue; // if the state name contains whitespace after the brace - var isBrace = label[i][0] === '('; - label[i] = label[i].substring(isBrace ? 1 : 0, isBrace ? 2 : 1); - } - labelString = label.join('').substring(0,6); - var counter = 1; - var len = labelString.length; - while (stateAbbreviations.includes(labelString)) { - labelString = labelString.substring(0,len) + "'" + counter++; - } - stateAbbreviations.push(labelString); - state.stateData.abbreviation = labelString; - }); } - function getRoleName(role) { - if (typeof role == 'string') { - return role; - } else if (role instanceof Role) { - return role.name; - } else { - return JSON.stringify(role); - } + if (typeof role == 'string') { + return role; + } + else if (role instanceof WF.Role) { + return role.name; + } + else { + return JSON.stringify(role); + } } - /** * Checks if two roles are equal. - * @param {*} role1 - * @param {*} role2 - * @returns + * @param role1 + * @param role2 + * @returns */ function equalRoles(role1, role2) { - 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']); - } else if (role1.tag == 'user') { - 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; -} - -/** - * - * @param {*} event - * @param {HTMLElement} menu - */ -function openContextMenu(x, y, menu) { - menu.style.top = y - 25; - menu.style.left = x + 20; - fadeIn(null, {element: menu, max: 1, step: 0.1}) - // menu.style.display = 'block'; - edgeFrom = edgeTo = null; -} - - -function runnn() { - - prepareWorkflow(); - -const edgeColourDefault = '#999999ff'; -const edgeColourSelected = '#000000ff'; -const edgeColourSelectedDarkMode = '#ffffffff'; -const edgeColourHighlightDefault = '#6ed4d4'; -const edgeColourHighlightSelected = 'magenta'; -const edgeColourSubtleDefault = '#99999955'; -const edgeColourSubtleSelected = '#00000055'; -const edgeColourSubtleSelectedDarkMode = '#ffffff55'; -const edgeColourMostSubtle = '#99999944'; - -/** - * - * @param {*} node - * @returns The colour the given node should have. - */ -function getNodeColour(node) { - var standard = (selectedActor.value === NO_ACTOR && selectedViewer.value === NO_VIEWER) - || highlightedSources.includes(node.id) || highlightedTargets.includes(node.id) - var alpha = standard ? 'ff' : '55'; - var isSelected = selection === node || rightSelection === node; - if (node.stateData && node.stateData.final !== 'False' && node.stateData.final !== '') { - if (node.stateData.final === 'True' || node.stateData.final === 'ok') { - return (isSelected ? '#3ac713' : '#31a810') + alpha; - } else if (node.stateData.final === 'not-ok') { - return (isSelected ? '#ec4e7b' : '#e7215a') + alpha; - } else { - return (isSelected ? '#ffbc15' : '#eeaa00') + alpha; + role1 instanceof WF.Role && (role1 = role1.json); + role2 instanceof WF.Role && (role2 = role2.json); + var equal = role1.tag === role2.tag; + if (role1.tag == 'payload-reference') { + equal = equal && (role1['payload-label'] === role2['payload-label']); } - } else if (node.name === '@@INIT') { - return (isSelected ? '#ffbc15' : '#eeaa00') + alpha; - } else { - return (isSelected ? '#538cd9' : '#3679d2') + alpha; - } + else if (role1.tag == 'user') { + equal = equal && (role1.user === role2.user); + } + else if (role1.tag == 'authorized') { + if (!role1.authorized || (role2.tag == 'authorized' && !role2.authorized)) { + console.error("Missing attribute 'authorized' for one of:", role1, role2); + equal = equal && (role1.authorized == role2.authorized); + } + else if (!role2.authorized) + equal = false; + else + equal = equal && (role1.authorized['dnf-terms'][0][0].var === role2.authorized['dnf-terms'][0][0].var); + } + return equal; } - -function isHighlightedActorEdge(edge) { - var data = edge.actionData; - var isActor = data.mode != 'automatic' && data.actorNames.includes(selectedActor.value); - var isActorAuto = data.mode == 'automatic' && highlightedTargets.includes(edge.source.id); - return isActor || isActorAuto; +function openContextMenu(x, y, menu) { + menu.style.top = (y - 25).toString(); + menu.style.left = (x + 20).toString(); + fadeIn(null, { element: menu, max: 1, step: 0.1 }); + // menu.style.display = 'block'; + edgeFrom = edgeTo = null; } - -function isHighlightedViewerEdge(edge) { - var data = edge.actionData; - return data.viewerNames.includes(selectedViewer.value); +function runnn() { + prepareWorkflow(); + const edgeColourDefault = '#999999ff'; + const edgeColourSelected = '#000000ff'; + const edgeColourSelectedDarkMode = '#ffffffff'; + const edgeColourHighlightDefault = '#6ed4d4'; + const edgeColourHighlightSelected = 'magenta'; + const edgeColourSubtleDefault = '#99999955'; + const edgeColourSubtleSelected = '#00000055'; + const edgeColourSubtleSelectedDarkMode = '#ffffff55'; + const edgeColourMostSubtle = '#99999944'; + /** + * + * @param node + * @returns The colour the given node should have. + */ + function getNodeColour(node) { + var standard = (selectedActor.value === NO_ACTOR && selectedViewer.value === NO_VIEWER) + || highlightedSources.includes(node.id) || highlightedTargets.includes(node.id); + var alpha = standard ? 'ff' : '55'; + var isSelected = selection === node || rightSelection === node; + if (node.stateData && node.stateData.final !== 'false' && node.stateData.final !== '') { + if (node.stateData.final === 'true' || node.stateData.final === 'ok') { + return (isSelected ? '#3ac713' : '#31a810') + alpha; + } + else if (node.stateData.final === 'not-ok') { + return (isSelected ? '#ec4e7b' : '#e7215a') + alpha; + } + else { + return (isSelected ? '#ffbc15' : '#eeaa00') + alpha; + } + } + else if (node.name === '@@INIT') { + return (isSelected ? '#ffbc15' : '#eeaa00') + alpha; + } + else { + return (isSelected ? '#538cd9' : '#3679d2') + alpha; + } + } + function isHighlightedActorEdge(edge) { + var data = edge.actionData; + var isActor = data.mode != 'automatic' && data.actorNames.includes(selectedActor.value); + var isActorAuto = data.mode == 'automatic' && highlightedTargets.includes(edge.source.id); + return isActor || isActorAuto; + } + function isHighlightedViewerEdge(edge) { + var data = edge.actionData; + return data.viewerNames.includes(selectedViewer.value); + } + function getEdgeColour(edge) { + var isSelected = selection === edge || rightSelection === edge; + if (isHighlightedActorEdge(edge)) { + return isSelected ? edgeColourHighlightSelected : edgeColourHighlightDefault; + } + else if (selectedViewer.value !== NO_VIEWER && !isHighlightedViewerEdge(edge)) { + return isSelected ? edgeColourSubtleDefault : edgeColourMostSubtle; + } + else if (selectedActor.value !== NO_ACTOR) { + return isSelected ? (darkMode ? edgeColourSubtleSelectedDarkMode : edgeColourSubtleSelected) + : edgeColourSubtleDefault; + } + else { + return isSelected ? (darkMode ? edgeColourSelectedDarkMode : edgeColourSelected) + : edgeColourDefault; + } + } + wfGraph + .linkDirectionalArrowLength(4) + .linkDirectionalArrowRelPos(1) + .linkColor(getEdgeColour) + .linkCurvature('curvature') + .linkCanvasObjectMode(() => 'after') + .linkCanvasObject((edge, context) => { + const MAX_FONT_SIZE = 4; + const LABEL_NODE_MARGIN = wfGraph.nodeRelSize() * edge.source.val * 1.5; + const source = edge.source; + const target = edge.target; + const curvature = edge.curvature; + var textPos = (source === target) + ? { x: source.x, y: source.y } + : { x: source.x + (target.x - source.x) / 2, + y: source.y + (target.y - source.y) / 2 }; + const edgeVector = { x: target.x - source.x, y: target.y - source.y }; + if (source !== target) { + var evLength = Math.sqrt(Math.pow(edgeVector.x, 2) + Math.pow(edgeVector.y, 2)); + var perpendicular = { x: edgeVector.x, y: (-Math.pow(edgeVector.x, 2) / edgeVector.y) }; + var pLength = Math.sqrt(Math.pow(perpendicular.x, 2) + Math.pow(perpendicular.y, 2)); + perpendicular.x = perpendicular.x / pLength; + perpendicular.y = perpendicular.y / pLength; + var fromSource = { x: source.x + perpendicular.x, y: source.y + perpendicular.y }; + // If source would cycle around target in clockwise direction, would fromSource point into this direction? + // If not, the perpendicular vector must be flipped in order to ensure that the label is displayed on the + // intended curved edge. + var isClockwise = (source.x < target.x && fromSource.y > source.y) || + (source.x > target.x && fromSource.y < source.y) || + (source.x === target.x && ((source.y < target.y && fromSource.x < source.x) || + source.y > target.y && fromSource.x > source.x)); + var offset = 0.5 * evLength * (isClockwise ? -curvature : curvature); + textPos = { x: textPos.x + perpendicular.x * offset, y: textPos.y + perpendicular.y * offset }; + } + else if (edge.__controlPoints) { // Position label relative to the Bezier control points of the self loop + edgeVector.x = edge.__controlPoints[2] - edge.__controlPoints[0]; + edgeVector.y = edge.__controlPoints[3] - edge.__controlPoints[1]; + var ctrlCenter = { x: edge.__controlPoints[0] + (edge.__controlPoints[2] - edge.__controlPoints[0]) / 2, + y: edge.__controlPoints[1] + (edge.__controlPoints[3] - edge.__controlPoints[1]) / 2 }; + var fromSource = { x: ctrlCenter.x - source.x, y: ctrlCenter.y - source.y }; + var fromSrcLen = Math.sqrt(Math.pow(fromSource.x, 2) + Math.pow(fromSource.y, 2)); + fromSource.x /= fromSrcLen; + fromSource.y /= fromSrcLen; + // The distance of the control point is 70 * curvature. Slightly more than half of it is appropriate here: + textPos = { x: source.x + fromSource.x * 37 * curvature, y: source.y + fromSource.y * 37 * curvature }; + } + const maxTextLength = (source !== target) ? Math.sqrt(Math.pow(edgeVector.x, 2) + Math.pow(edgeVector.y, 2)) - LABEL_NODE_MARGIN + : 1.5 * Math.sqrt(4 * source.val + 100 * curvature); + var textAngle = Math.atan2(edgeVector.y, edgeVector.x); + // maintain label vertical orientation for legibility + if (textAngle > Math.PI / 2) + textAngle = -(Math.PI - textAngle); + if (textAngle < -Math.PI / 2) + textAngle = -(-Math.PI - textAngle); + var label = edge.name; + // estimate fontSize to fit in link length + //context.font = '1px Sans-Serif'; + const fontSize = MAX_FONT_SIZE; // Math.min(MAX_FONT_SIZE, maxTextLength / context.measureText(label).width); + context.font = `${fontSize}px Inter`; + var textLen = context.measureText(label).width; + if (textLen > maxTextLength) { + var allowedLen = maxTextLength * (label.length / textLen); + label = label.substring(0, allowedLen); + if (label !== edge.name) + label += '...'; + textLen = context.measureText(label).width; + } + const bckgDimensions = [textLen, fontSize]; + // draw text label (with background rect) + context.save(); + context.translate(textPos.x, textPos.y); + context.rotate(textAngle); + context.fillStyle = darkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)'; + context.fillRect(-bckgDimensions[0] / 2, -bckgDimensions[1] / 2, ...bckgDimensions); + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillStyle = getEdgeColour(edge); + context.fillText(label, 0, 0); + context.restore(); + }) + .linkLineDash((edge) => edge.actionData.mode == 'automatic' && [2, 3]) //[dash, gap] + .linkWidth((edge) => ((edge === selection || edge === rightSelection) ? 2 : 0) + ((isHighlightedActorEdge(edge) || isHighlightedViewerEdge(edge)) ? 4 : 1)) + .linkDirectionalParticles(2) + .linkDirectionalParticleColor(() => darkMode ? '#ffffff55' : '#00000055') + .linkDirectionalParticleWidth((edge) => (isHighlightedActorEdge(edge)) ? 3 : 0) + .nodeCanvasObject((node, ctx) => { + ctx.fillStyle = getNodeColour(node); + ctx.beginPath(); + ctx.arc(node.x, node.y, 2 * node.val, 0, 2 * Math.PI, false); + ctx.fill(); + if (node === selection || node === rightSelection) { + ctx.strokeStyle = darkMode ? 'white' : 'black'; + ctx.lineWidth = 1; + ctx.setLineDash([1, 2]); + ctx.lineCap = 'round'; + ctx.stroke(); + } + if (!(node.stateData && node.stateData.abbreviation)) + return; + ctx.fillStyle = 'white'; + ctx.font = '4px Inter'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(node.stateData.abbreviation, node.x, node.y); + }) + .onNodeDragEnd((node) => { + node.fx = node.x; + node.fy = node.y; + }) + .onNodeClick((node, _) => { + if (edgeFrom) { + connect(edgeFrom, node); + edgeFrom = null; + } + else if (edgeTo) { + connect(node, edgeTo); + edgeTo = null; + } + else + select(node); + closeMenuItem(); + }) + .onNodeRightClick((node, event) => { + openContextMenu(event.layerX, event.layerY, contextMenuSt); + closeContextMenus(contextMenuBg, contextMenuEd); + // contextMenuBg.style.display = contextMenuEd.style.display = 'none'; + rightSelection = node; + closeMenuItem(); + }) + .onLinkClick((edge, _) => { + select(edge); + closeMenuItem(); + }) + .onLinkRightClick((edge, event) => { + openContextMenu(event.layerX, event.layerY, contextMenuEd); + closeContextMenus(contextMenuBg, contextMenuSt); + // contextMenuBg.style.display = contextMenuSt.style.display = 'none'; + rightSelection = edge; + closeMenuItem(); + }) + .onBackgroundClick((_) => { + closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); + deselect(); + edgeFrom = edgeTo = rightSelection = null; + closeMenuItem(); + }) + .onBackgroundRightClick((event) => { + newStateCoords = wfGraph.screen2GraphCoords(event.layerX, event.layerY); + openContextMenu(event.layerX, event.layerY, contextMenuBg); + closeContextMenus(contextMenuEd, contextMenuSt); + // contextMenuEd.style.display = contextMenuSt.style.display = 'none'; + edgeFrom = edgeTo = rightSelection = null; + closeMenuItem(); + }) + .autoPauseRedraw(false); + updateGraph(); } - -function getEdgeColour(edge) { - var isSelected = selection === edge || rightSelection === edge; - if (isHighlightedActorEdge(edge)) { - return isSelected ? edgeColourHighlightSelected : edgeColourHighlightDefault; - } else if (selectedViewer.value !== NO_VIEWER && !isHighlightedViewerEdge(edge)) { - return isSelected ? edgeColourSubtleDefault : edgeColourMostSubtle; - } else if (selectedActor.value !== NO_ACTOR) { - return isSelected ? (darkMode ? edgeColourSubtleSelectedDarkMode : edgeColourSubtleSelected) - : edgeColourSubtleDefault; - } else { - return isSelected ? (darkMode ? edgeColourSelectedDarkMode : edgeColourSelected) - : edgeColourDefault; - } -} - wfGraph - .linkDirectionalArrowLength(4) - .linkDirectionalArrowRelPos(1) - .linkColor(getEdgeColour) - .linkCurvature('curvature') - .linkCanvasObjectMode(() => 'after') - .linkCanvasObject((edge, context) => { - const MAX_FONT_SIZE = 4; - const LABEL_NODE_MARGIN = wfGraph.nodeRelSize() * edge.source.val * 1.5; - - const source = edge.source; - const target = edge.target; - const curvature = edge.curvature || 0; - - var textPos = (source === target) ? {x: source.x, y: source.y} : Object.assign(...['x', 'y'].map(c => ({ - [c]: source[c] + (target[c] - source[c]) / 2 - }))); - - const edgeVector = {x: target.x - source.x, y: target.y - source.y}; - if (source !== target) { - var evLength = Math.sqrt(Math.pow(edgeVector.x, 2) + Math.pow(edgeVector.y, 2)); - var perpendicular = {x: edgeVector.x, y: (-Math.pow(edgeVector.x, 2) / edgeVector.y)}; - var pLength = Math.sqrt(Math.pow(perpendicular.x, 2) + Math.pow(perpendicular.y, 2)); - perpendicular.x = perpendicular.x / pLength; - perpendicular.y = perpendicular.y / pLength; - var fromSource = {x: source.x + perpendicular.x, y: source.y + perpendicular.y}; - // If source would cycle around target in clockwise direction, would fromSource point into this direction? - // If not, the perpendicular vector must be flipped in order to ensure that the label is displayed on the - // intended curved edge. - var isClockwise = (source.x < target.x && fromSource.y > source.y) || - (source.x > target.x && fromSource.y < source.y) || - (source.x === target.x && ((source.y < target.y && fromSource.x < source.x) || - source.y > target.y && fromSource.x > source.x)); - var offset = 0.5 * evLength * (isClockwise ? -curvature : curvature); - textPos = {x: textPos.x + perpendicular.x * offset, y: textPos.y + perpendicular.y * offset}; - } else if (edge.__controlPoints) { // Position label relative to the Bezier control points of the self loop - edgeVector.x = edge.__controlPoints[2] - edge.__controlPoints[0]; - edgeVector.y = edge.__controlPoints[3] - edge.__controlPoints[1]; - var ctrlCenter = {x: edge.__controlPoints[0] + (edge.__controlPoints[2] - edge.__controlPoints[0]) / 2, - y: edge.__controlPoints[1] + (edge.__controlPoints[3] - edge.__controlPoints[1]) / 2}; - var fromSource = {x: ctrlCenter.x - source.x, y: ctrlCenter.y - source.y}; - var fromSrcLen = Math.sqrt(Math.pow(fromSource.x, 2) + Math.pow(fromSource.y, 2)); - fromSource.x /= fromSrcLen; - fromSource.y /= fromSrcLen; - // The distance of the control point is 70 * curvature. Slightly more than half of it is appropriate here: - textPos = {x: source.x + fromSource.x * 37 * curvature, y: source.y + fromSource.y * 37 * curvature}; - } - - const maxTextLength = (source !== target) ? Math.sqrt(Math.pow(edgeVector.x, 2) + Math.pow(edgeVector.y, 2)) - LABEL_NODE_MARGIN - : 1.5 * Math.sqrt(4 * source.val + 100 * curvature); - - var textAngle = Math.atan2(edgeVector.y, edgeVector.x); - // maintain label vertical orientation for legibility - if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle); - if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle); - - var label = edge.name; - - // estimate fontSize to fit in link length - //context.font = '1px Sans-Serif'; - const fontSize = MAX_FONT_SIZE;// Math.min(MAX_FONT_SIZE, maxTextLength / context.measureText(label).width); - context.font = `${fontSize}px Inter`; - - var textLen = context.measureText(label).width; - if (textLen > maxTextLength) { - var allowedLen = maxTextLength * (label.length / textLen); - label = label.substring(0, allowedLen); - if (label !== edge.name) label += '...'; - textLen = context.measureText(label).width; - } - - const bckgDimensions = [textLen, fontSize]; - - // draw text label (with background rect) - context.save(); - context.translate(textPos.x, textPos.y); - context.rotate(textAngle); - - context.fillStyle = darkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)'; - context.fillRect(- bckgDimensions[0] / 2, - bckgDimensions[1] / 2, ...bckgDimensions); - - context.textAlign = 'center'; - context.textBaseline = 'middle'; - context.fillStyle = getEdgeColour(edge); - context.fillText(label, 0, 0); - context.restore(); - }) - .linkLineDash(edge => edge.actionData.mode == 'automatic' && [2, 3]) //[dash, gap] - .linkWidth(edge => (((edge === selection || edge === rightSelection) ? 2 : 0) + (isHighlightedActorEdge(edge) || isHighlightedViewerEdge(edge)) ? 4 : 1)) - .linkDirectionalParticles(2) - .linkDirectionalParticleColor(() => darkMode ? '#ffffff55' : '#00000055') - .linkDirectionalParticleWidth(edge => (isHighlightedActorEdge(edge)) ? 3 : 0) - .nodeCanvasObject((node, ctx) => { - ctx.fillStyle = getNodeColour(node); - ctx.beginPath(); - ctx.arc(node.x, node.y, 2*node.val, 0, 2 * Math.PI, false); - ctx.fill(); - if (node === selection || node === rightSelection) { - ctx.strokeStyle = darkMode ? 'white' : 'black'; - ctx.lineWidth = 1; - ctx.setLineDash([1, 2]); - ctx.lineCap = 'round'; - ctx.stroke(); - } - - if (! (node.stateData && node.stateData.abbreviation)) return; - - ctx.fillStyle = 'white'; - ctx.font = '4px Inter'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(node.stateData.abbreviation, node.x, node.y); - }) - .onNodeDragEnd(node => { - node.fx = node.x; - node.fy = node.y; - }) - .onNodeClick((node, _) => { - if (edgeFrom) { - connect(edgeFrom, node); - edgeFrom = null; - } else if (edgeTo) { - connect(node, edgeTo); - edgeTo = null; - } else select(node); - closeMenuItem(); - }) - .onNodeRightClick((node, event) => { - openContextMenu(event.layerX, event.layerY, contextMenuSt); - closeContextMenus(contextMenuBg, contextMenuEd); - // contextMenuBg.style.display = contextMenuEd.style.display = 'none'; - rightSelection = node; - closeMenuItem(); - }) - .onLinkClick((edge, _) => { - select(edge); - closeMenuItem(); - }) - .onLinkRightClick((edge, event) => { - openContextMenu(event.layerX, event.layerY, contextMenuEd); - closeContextMenus(contextMenuBg, contextMenuSt); - // contextMenuBg.style.display = contextMenuSt.style.display = 'none'; - rightSelection = edge; - closeMenuItem() - }) - .onBackgroundClick(_ => { - closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); - deselect(); - edgeFrom = edgeTo = rightSelection = null; - closeMenuItem(); - }) - .onBackgroundRightClick(event => { - newStateCoords = wfGraph.screen2GraphCoords(event.layerX, event.layerY); - openContextMenu(event.layerX, event.layerY, contextMenuBg); - closeContextMenus(contextMenuEd, contextMenuSt); - // contextMenuEd.style.display = contextMenuSt.style.display = 'none'; - edgeFrom = edgeTo = rightSelection = null; - closeMenuItem(); - }) - .autoPauseRedraw(false); - -updateGraph(); - } \ No newline at end of file +//Keyboard commands +document.addEventListener('keydown', e => { + console.log(e.ctrlKey, e.key); + if (e.key === 'Escape') { + closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); + closeMenuItem(); + closeFileDisplay(); + deselect(); + rightSelection = null; + searchInput.blur(); + } + else if (!e.ctrlKey) + return; + switch (e.key) { + case 'f': + e.preventDefault(); + searchInput.focus(); + openSearchMenu(searchContainer.parentElement); + break; + case 'o': + e.preventDefault(); + openFileDisplay(); + default: + break; + } +}); diff --git a/editor.ts b/editor.ts new file mode 100644 index 0000000..490877a --- /dev/null +++ b/editor.ts @@ -0,0 +1,1180 @@ +import * as WF from './workflow.js'; +import './forcegraph.js'; +import 'https://unpkg.com/force-graph@1.43.0/dist/force-graph.min.js'; +//@ts-ignore +import Index from './node_modules/flexsearch/src/index.js'; // 'https://cdn.jsdelivr.net/gh/nextapps-de/flexsearch@0.7.31/dist/flexsearch.bundle.js' + +//Theme +var darkMode = false; + +export function toggleTheme() { + darkMode = !darkMode; + + var menus = [mainMenu, sidePanel, filePanel]; + Array.from(document.getElementsByClassName('menuitem')).forEach(item => + item !== fileMenu && + Array.from(item.getElementsByClassName('submenu')).forEach(subMenu => menus.push(' + workflowFiles[i].name + '' + workflowFiles[i].description; + var url = 'http://localhost:8080/spaß' + workflowFiles[i].url; + defineOnClick(item, url, workflowFiles[i].name); + fileContent.appendChild(item); + } + var url = 'http://localhost:8080/spaß' + workflowFiles[0].url; + return fetch(url); + }) + .then((response) => response.json()) + .then((data) => { + ( |