diff --git a/editor.js b/editor.js deleted file mode 100644 index 41549ea..0000000 --- a/editor.js +++ /dev/null @@ -1,1111 +0,0 @@ -// SPDX-FileCopyrightText: 2023 David Mosbach -// -// SPDX-License-Identifier: AGPL-3.0-or-later -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(subMenu))); - Array.from(fileMenu.children).forEach(child => menus.push(child)); - Array.from(document.getElementById('editmenu').children).forEach(child => menus.push(child)); - Array.from(contextMenuBg.children).forEach(child => menus.push(child)); - Array.from(contextMenuEd.children).forEach(child => menus.push(child)); - Array.from(contextMenuSt.children).forEach(child => menus.push(child)); - Array.from(document.getElementsByClassName('graph-tooltip')).forEach(tooltip => menus.push(tooltip)); - var contentHints = [ - document.getElementById('filename'), - document.getElementById('sidecontentedge'), - document.getElementById('sidecontentnode') - ]; - var searchIcon = document.getElementById('search-icon'); - if (darkMode) { - menus.forEach(target => { - target?.classList.add('menu-darkmode'); - target?.classList.remove('menu-lightmode'); - }); - contentHints.forEach(hint => { - hint?.classList.add('contenttype-darkmode'); - hint?.classList.remove('contenttype-lightmode'); - }); - searchIcon?.classList.add('search-icon-darkmode'); - searchIcon?.classList.remove('search-icon-lightmode'); - wfGraph.backgroundColor('black'); - } - else { - menus.forEach(target => { - target?.classList.add('menu-lightmode'); - target?.classList.remove('menu-darkmode'); - }); - contentHints.forEach(hint => { - hint?.classList.add('contenttype-lightmode'); - hint?.classList.remove('contenttype-darkmode'); - }); - searchIcon?.classList.add('search-icon-lightmode'); - searchIcon?.classList.remove('search-icon-darkmode'); - wfGraph.backgroundColor('white'); - } -} -// Menu bar -const mainMenu = document.getElementById('mainmenu'); -var selectedMenuItem = null; -Array.from(document.getElementsByClassName('submenu')) - .forEach(subMenu => subMenu.style.top = (mainMenu.offsetHeight + 15).toString()); -var lastSubMenu = null; -function positionSubmenuBackdrop() { - if (!lastSubMenu || !submenuBackdrop) - return; - var smRect = lastSubMenu.getBoundingClientRect(); - submenuBackdrop.style.top = smRect.top.toString(); // sideHeading.offsetHeight + parseFloat(smStyle.paddingTop) + parseFloat(shStyle.marginTop) + parseFloat(shStyle.marginBottom); - submenuBackdrop.style.left = smRect.left.toString(); - submenuBackdrop.style.width = lastSubMenu.offsetWidth.toString(); - submenuBackdrop.style.height = lastSubMenu.offsetHeight.toString(); - // var sbStyle = window.getComputedStyle(sideButtons); - // sideContent.style.bottom = sideButtons.offsetHeight + parseFloat(smStyle.paddingBottom) + parseFloat(sbStyle.marginTop) + parseFloat(sbStyle.marginBottom); - // console.log(sideHeading.offsetHeight + shStyle.marginTop + shStyle.marginBottom); - // var width = -} -/** - * - * @param {HTMLElement} menuitem - */ -function openMenuItem(menuitem) { - edgeTo = edgeFrom = rightSelection = null; - closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); - if (menuitem === selectedMenuItem) { - closeMenuItem(); - return; - } - var fadeOuts = []; - Array.from(document.getElementsByClassName('selectedmenuitem')).forEach(other => { - other.classList.remove('selectedmenuitem'); - Array.from(other.getElementsByClassName('submenu')).forEach(subMenu => fadeOuts.push({ element: subMenu, min: 0, step: 0.1 })); - }); - fadeOut(null, ...fadeOuts); - menuitem.classList.add('selectedmenuitem'); - var fadeIns = [{ element: submenuBackdrop, max: 1 }]; - Array.from(menuitem.getElementsByClassName('submenu')).forEach(subMenu => { - fadeIns.push({ element: subMenu, max: 1 }); - lastSubMenu = subMenu; - }); - fadeIn(positionSubmenuBackdrop, ...fadeIns); - selectedMenuItem = menuitem; -} -function closeContextMenus(...menus) { - var items = []; - menus.forEach(menu => items.push({ element: menu, min: 0, step: 0.1 })); - fadeOut(null, ...items); -} -function closeMenuItem() { - if (!selectedMenuItem) - return; - selectedMenuItem.classList.remove('selectedmenuitem'); - var fadeOuts = [{ element: submenuBackdrop, min: 0, step: 0.1 }]; - Array.from(selectedMenuItem.getElementsByClassName('submenu')).forEach(subMenu => fadeOuts.push({ element: subMenu, min: 0, step: 0.1 })); - fadeOut(() => searchResults.style.display = 'none', ...fadeOuts); - selectedMenuItem = null; -} -export function openFileMenu(menuitem) { - openMenuItem(menuitem); -} -export function openEditMenu(menuitem) { - openMenuItem(menuitem); -} -export function openViewMenu(menuitem) { - openMenuItem(menuitem); -} -export function openSettingsMenu(menuitem) { - openMenuItem(menuitem); -} -export function openAboutMenu(menuitem) { - openMenuItem(menuitem); -} -function focusSelection() { - if (!selection) - return; - var x = 0; - var y = 0; - if (selection.hasOwnProperty('actionData')) { - selection = selection; - x = selection.source.x + (selection.target.x - selection.source.x) / 2; - y = selection.source.y + (selection.target.y - selection.source.y) / 2; - } - else { - selection = selection; - x = selection.x; - y = selection.y; - } - wfGraph.centerAt(x, y, 400); - wfGraph.zoom(5, 400); -} -export function openSearchMenu(menuitem) { - if (selectedMenuItem === menuitem) - return; - var val = searchInput.value; - if (val === '' || val === null) - while (searchResultList.firstChild) - searchResultList.removeChild(searchResultList.lastChild); - openMenuItem(menuitem); -} -document.getElementById('filepanel').style.opacity = '0'; -//Search -var nodeIndex = new Index({ tokenize: 'forward' }); -var actionIndex = new Index({ tokenize: 'forward' }); -// const searchDocument = new FlexSearch.Document(); -// const searchWorker = new FlexSearch.Worker(); -const soStates = document.getElementById('search-option-states'); -const soEdges = document.getElementById('search-option-edges'); -export function search(text) { - while (searchResultList.firstChild) - searchResultList.removeChild(searchResultList.lastChild); - var searchStates = soStates.checked; - var searchActions = soEdges.checked; - var stateResults = searchStates ? nodeIndex.search(text, searchActions ? 5 : 10) : null; - var actionResults = searchActions ? actionIndex.search(text, searchStates ? 5 : 10) : null; - function defineFocus(div, target) { - div.onclick = (_ => { - searchInput.value = ''; - closeMenuItem(); - select(target); - focusSelection(); - }); - } - function format(possibleTargets, results, heading) { - var h = document.createElement('h3'); - h.innerHTML = heading; - searchResultList.appendChild(h); - results.forEach(result => { - var target = null; - possibleTargets.forEach(stateOrEdge => { - if (stateOrEdge.id === result) - target = stateOrEdge; - }); - if (!target) - return; - var r = document.createElement('div'); - var head = document.createElement('div'); - head.innerText = target.name; - head.classList.add('search-result-head'); - r.appendChild(head); - var info = document.createElement('div'); - if (target.hasOwnProperty('actionData')) - info.innerText = target.source.name + ' → ' + target.target.name; - else - info.innerText = target.stateData.abbreviation; - info.setAttribute('title', info.innerText); - info.classList.add('search-result-info'); - r.appendChild(info); - searchResultList.appendChild(r); - defineFocus(r, target); - }); - } - searchResultList.style.maxHeight = (parseFloat(searchResults.style.maxHeight) - searchOptions.offsetHeight).toString(); - // console.log('maxh', searchResults.style.maxHeight - searchOptions.offsetHeight) - stateResults && format(workflow.states, stateResults, 'States'); - actionResults && format(workflow.actions, actionResults, 'Edges'); - positionSubmenuBackdrop(); -} -export function showSearchResults() { - searchResults.style.display = 'block'; -} -export function openFileDisplay() { - deselect(); - function callback() { - fileHeading.innerHTML = 'Open Workflow Definition'; - var pStyle = window.getComputedStyle(filePanel); - var hStyle = window.getComputedStyle(fileHeading); - fileContent.style.top = (fileHeading.offsetHeight + parseFloat(pStyle.paddingTop) + parseFloat(hStyle.marginTop) + parseFloat(hStyle.marginBottom)).toString(); - var bStyle = window.getComputedStyle(fileButtons); - fileContent.style.bottom = (fileButtons.offsetHeight + parseFloat(pStyle.paddingBottom) + parseFloat(bStyle.marginTop) + parseFloat(bStyle.marginBottom)).toString(); - } - fadeIn(callback, { element: filePanel, max: 1, step: 0.025 }, { element: curtain, max: 0.5, step: 0.025 }); - closeMenuItem(); -} -export function closeFileDisplay() { - var panel = document.getElementById('filepanel'); - fadeOut(null, { element: panel, min: 0, step: 0.025 }, { element: curtain, min: 0, step: 0.025 }); -} -function fadeIn(callback, ...items) { - requestAnimationFrame(() => { - items.forEach(i => i.element && (i.element.style.display = 'block')); - if (callback) - callback(); - }); - function fade() { - var proceed = false; - items.forEach(i => { - if (!i.element || i.max === undefined) - return; - var newOpacity = (parseFloat(i.element.style.opacity) || 0) + (i.step || 0.05); - if (newOpacity <= i.max) { - i.element.style.opacity = newOpacity.toString(); - // console.log(i.max, i.element.style.opacity) - proceed = true; - } - else if ((parseFloat(i.element.style.opacity) || 0) > i.max) - i.element.style.opacity = i.max.toString(); - }); - if (proceed) - requestAnimationFrame(fade); - } - requestAnimationFrame(fade); -} -function fadeOut(callback, ...items) { - function fade() { - var proceed = false; - items.forEach(i => { - if (!i.element || i.min === undefined) - return; - var newOpacity = (parseFloat(i.element.style.opacity) || 0) - (i.step || 0.05); - if (newOpacity >= i.min) { - i.element.style.opacity = newOpacity.toString(); - // console.log(i.max, i.element.style.opacity) - proceed = true; - } - else { - if ((parseFloat(i.element.style.opacity) || 0) < i.min) - i.element.style.opacity = i.min.toString(); - i.element.style.display = 'none'; - } - }); - if (proceed) - requestAnimationFrame(fade); - else if (callback) - requestAnimationFrame(callback); - } - requestAnimationFrame(fade); -} -// Available workflow definition files -var workflowFiles = []; -// Workflow data -var workflow = new WF.Workflow({ - states: [], - actions: [] -}); -//@ts-ignore -const wfGraph = ForceGraph(); -function defineOnClick(item, url, title) { - item.onclick = (_ => { - fetch(url) - .then(response => response.json()) - .then(data => { - closeFileDisplay(); - searchInput.value = ''; - while (searchResultList.firstChild) - searchResultList.removeChild(searchResultList.lastChild); - workflow = new WF.Workflow({ - states: data.states, - actions: data.actions - }); - nodeIndex = new Index({ tokenize: 'forward' }); - actionIndex = new Index({ tokenize: 'forward' }); - prepareWorkflow(); - updateGraph(); - wfGraph.centerAt(0, 0, 400); - wfGraph.zoom(1, 400); - document.getElementById('filename').innerText = title; - document.title = title + ' | Editor'; - }); - }); -} -fetch('http://localhost:8080/spaß/index.json') - .then(response => response.json()) - .then(data => { - workflowFiles = data; - for (var i = 0; i < workflowFiles.length; i++) { - 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/spaß' + workflowFiles[0].url; - return fetch(url); -}) - .then((response) => response.json()) - .then((data) => { - document.getElementById('filename').innerText = workflowFiles[0].name; - document.title = workflowFiles[0].name + ' | Editor'; - 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'); -//Viewers of the workflow -var viewers = []; -const selectedViewer = document.getElementById('viewer'); -//Actions/States with no explicit viewers -var viewableByAll = []; -//Possible 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 = []; -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); - } - }); -} -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. -var edgeFrom = null; // Start on an edge to be created. -//Utility elements -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 sideInfoEdge = document.getElementById('sidecontentedge'); -const sideInfoNode = document.getElementById('sidecontentnode'); -//File panel -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 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'); -// 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 -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).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. - */ -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 }); - } - 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'; - selection = null; -} -export function rightSelect() { - if (!rightSelection) - return; - select(rightSelection); -} -/** - * Adds a new state to the workflow and auto-selects it. - */ -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. - */ -function connect(source, target) { - 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); -} -export function markEdgeTo() { - edgeTo = rightSelection; - closeContextMenus(contextMenuSt); - // contextMenuSt.style.display = 'none'; -} -export function markEdgeFrom() { - edgeFrom = rightSelection; - closeContextMenus(contextMenuSt); - // contextMenuSt.style.display = 'none'; -} -export function removeSelection() { - if (selection) { - if (selection.hasOwnProperty('actionData')) - removeAction(selection); - else - removeState(selection); - deselect(); - edgeFrom = edgeTo = rightSelection = null; - closeContextMenus(contextMenuEd, contextMenuSt, contextMenuBg); - } -} -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. - */ -function removeAction(action) { - 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); -} -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 }); -} -/** - * Identifies and stores self loops as well as overlapping edges (i.e. multiple edges sharing the - * same source and target). - */ -function identifyOverlappingEdges() { - 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 - 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; - }); -} -function getRoleName(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 - */ -function equalRoles(role1, role2) { - 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 (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 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 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(); -} -//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/forcegraph.js b/forcegraph.js deleted file mode 100644 index 4d177f3..0000000 --- a/forcegraph.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; -// SPDX-FileCopyrightText: 2023 David Mosbach -// -// SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/workflow.js b/workflow.js deleted file mode 100644 index 7c7e5f4..0000000 --- a/workflow.js +++ /dev/null @@ -1,285 +0,0 @@ -// SPDX-FileCopyrightText: 2023 David Mosbach -// -// SPDX-License-Identifier: AGPL-3.0-or-later -export class Workflow { - states; - actions; - constructor(json) { - const stateMap = new Map(); - this.states = []; - for (const state of json.states) { - var node = new WFNode(state); - this.states.push(node); - stateMap.set(node.id, node); - } - // Resolve ID refs to nodes - var actions = json.actions.map(action => { - function transformRef(ref) { - if (ref instanceof String) - stateMap.has(ref) - && (ref = stateMap.get(ref)) - || console.error('Ref to unknown state: ' + ref); - return ref; - } - action.source = transformRef(action.source); - action.target = transformRef(action.target); - return action; - }); - this.actions = []; - for (const action of actions) - this.actions.push(new WFEdge(action)); - } -} -export class WFNode { - stateData; - x; - y; - fx; - fy; - id; - name; - val; - constructor(json) { - this.stateData = new StateData(json.stateData); - this.x = json.x; - this.y = json.y; - this.fx = json.fx; - this.fy = json.fy; - this.id = json.id; - this.name = json.name; - this.val = json.val; - } -} -export class StateData { - abbreviation; - messages; - viewers; - payload; - viewerNames; - final; - constructor(json) { - this.abbreviation = json.abbreviation; - this.messages = json.messages ? json.messages.map(message => { return new Message(message); }) : []; - this.viewers = json.viewers ? new Viewers(json.viewers) : Viewers.empty(); - this.payload = new Payload(json.payload); - this.viewerNames = []; - this.final = json.final; - } -} -export class WFEdge { - actionData; - source; - target; - id; - name; - nodePairId; - curvature; - __controlPoints; - constructor(json) { - this.actionData = new ActionData(json.actionData); - this.source = json.source; - this.target = json.target; - this.id = json.id; - this.name = json.name; - this.nodePairId = json.nodePairId; - this.curvature = 0; - } -} -export class ActionData { - messages; - viewers; - actors; - 'actor Viewers'; - form; - viewerNames; - actorNames; - mode; - constructor(json) { - this.messages = json.messages ? json.messages.map(message => { return new Message(message); }) : []; - this.viewers = json.viewers ? new Viewers(json.viewers) : Viewers.empty(); - this.actors = json.actors ? new Actors(json.actors) : Actors.empty(); - this['actor Viewers'] = json['actor Viewers'] ? new Viewers(json['actor Viewers']) : Viewers.empty(); - this.form = new Payload(json.form); - this.viewerNames = []; - this.actorNames = []; - this.mode = json.mode ?? undefined; - } -} -export class Role { - json; - name; - constructor(json) { - this.json = json; - if (json.tag == 'payload-reference') { - this.name = json['payload-label']; - } - else if (json.authorized) { - this.name = json.authorized['dnf-terms'][0][0].var + ' (auth)'; //TODO ugly - } - else if (json.user) { - this.name = json.user; - } - else if (json.tag) { - this.name = json.tag + ' (tag)'; - } - else { - this.name = JSON.stringify(json); - } - } - format() { - return [document.createTextNode(this.name)]; - } -} -export class Roles { - roleName; - anchor; - comment; - roles; - constructor(json, roleName) { - this.roleName = roleName; - this.anchor = json.anchor ? new Anchor(json.anchor) : new Anchor('NoAnchor'); - this.roles = []; - for (const role of json[roleName]) - this.roles.push(new Role(role)); - this.comment = json.comment; - } - length() { - return this.roles.length; - } - format() { - var r = document.createElement('h4'); - var roles = document.createTextNode('Roles'); - r.appendChild(roles); - var rolesList = document.createElement('ul'); - this.roles.forEach(r => { - var role = document.createElement('li'); - role.appendChild(document.createTextNode(r.name)); - rolesList.appendChild(role); - }); - var result = []; - if (this.comment.length > 0) { - var c = document.createElement('h4'); - c.innerText = 'Comment'; - var comment = document.createElement('p'); - comment.innerText = this.comment.join(' '); - result.push(c, comment); - } - if (this.anchor) { - var a = document.createElement('h4'); - a.appendChild(this.anchor.format()); - result.push(a); - } - else - result.push(r); - result.push(rolesList); - return result; - } -} -export class Viewers extends Roles { - static empty() { - return new Viewers({ - viewers: [], - anchor: 'NoAnchor', - comment: [] - }); - } - constructor(json) { - super(json, 'viewers'); - } -} -export class Actors extends Roles { - static empty() { - return new Actors({ - actors: [], - anchor: 'NoAnchor', - comment: [] - }); - } - constructor(json) { - super(json, 'actors'); - } -} -export class Anchor { - name; - type; - constructor(json) { - if (!json || json === 'NoAnchor') { - this.name = undefined; - this.type = 'none'; - } - else { - this.name = json.name; - this.type = json.type; - } - } - format() { - return document.createTextNode(`${this.type == 'alias' ? '*' : '&'}${this.name}`); - } -} -export class Message { - fallback; - fallbackLang; - translations; - status; - viewers; - constructor(json) { - var content = json.content; - this.fallback = content.fallback; - this.fallbackLang = content['fallback-lang']; - this.translations = content.translations; - this.status = json.status; - this.viewers = new Viewers(json.viewers); - } - format() { - var v = document.createElement('h3'); - var viewers = document.createTextNode('Viewers'); - v.appendChild(viewers); - var viewerList = this.viewers.format(); - var h = document.createElement('h3'); - var heading = document.createTextNode('Status'); - h.appendChild(heading); - var p = document.createElement('p'); - var text = document.createTextNode(this.status); - p.appendChild(text); - var result = [v]; - result = result.concat(viewerList); - result.push(h, p); - h = document.createElement('h3'); - heading = document.createTextNode(this.fallbackLang); - h.appendChild(heading); - p = document.createElement('html'); - p.setAttribute('lang', this.fallbackLang); - p.innerHTML = this.fallback; - result.push(h, p); - for (var t in this.translations) { - h = document.createElement('h3'); - heading = document.createTextNode(t); - h.appendChild(heading); - p = document.createElement('html'); - p.setAttribute('lang', this.translations[t]); - p.innerHTML = this.translations[t]; - result.push(h, p); - } - return result; - } -} -export class Payload { - fields; - constructor(json) { - this.fields = []; - if (json === undefined) - return; - for (var f in json) { - this.fields.push(f); - } - } - format() { - var fieldList = document.createElement('ul'); - this.fields.forEach(f => { - var field = document.createElement('li'); - field.appendChild(document.createTextNode(f)); - fieldList.appendChild(field); - }); - return [fieldList]; - } -}