Draw.loadPlugin(function(ui) { const WEE = "https://wee.rys.app"; const BACKEND = "https://dtb.rys.app"; const EXPECTED_BACKEND_VERSION = 6; // expected backend version const SPARQL_SERVER = "https://fuseki.rys.app" const SPARQL_ENDPOINT = "/SystemDesignOntology2Layers/" const dropVocabularyPrefix = str => str.substring(41); const dropDescriptionPrefix = str => str.substring(30); const dropArtifactPrefix = str => str.substring(41); const addFormalismsPrefix = str => "http://ua.be/sdo2l/vocabulary/formalisms/" + str; const addArtifactPrefix = str => "http://ua.be/sdo2l/description/artifacts/" + str; const QUERIES = { // Query that navigates the given link from the given source element, and returns the IRI and most-concrete-type of the target element. // If `reverse` is true, then incoming links are returned instead. getOutgoingLink: (iri, link_type, reverse=false) => `\ PREFIX object_diagram: PREFIX rdfs: PREFIX pt: SELECT DISTINCT ?element ?type WHERE { ${reverse ? `?element <${link_type}> <${iri}>` : `<${iri}> <${link_type}> ?element`}. ?element a ?type . { ?type rdfs:subClassOf object_diagram:Object . } UNION { ?type rdfs:subClassOf pt:Event . } UNION { ?type rdfs:subClassOf pt:Artifact . } FILTER(NOT EXISTS { ?more_concrete_type rdfs:subClassOf ?type . ?element a ?more_concrete_type . FILTER(?more_concrete_type != ?type) }) FILTER(NOT EXISTS { ?more_concrete_link_type rdfs:subPropertyOf <${link_type}> . ${reverse ? `?element ?more_concrete_link_type <${iri}>` : `<${iri}> ?more_concrete_link_type ?element`} . FILTER(?more_concrete_link_type != <${link_type}>) }) }`, // Query that gets the IRI and most-concrete-type of a given drawio cell. getCellStuff: (cellId) => `\ PREFIX drawio: PREFIX object_diagram: PREFIX rdfs: SELECT DISTINCT ?element ?type WHERE { ?element drawio:hasDrawioId "${cellId}" . ?element a ?type . ?type rdfs:subClassOf object_diagram:Object . NOT EXISTS { ?more_concrete_type rdfs:subClassOf ?type . ?element a ?more_concrete_type . FILTER(?more_concrete_type != ?type) } }`, // Query that, for a given cell IRI, gets the name of the diagram containing the cell, and the ID of the cell. getDiagramAndCellId: cellIri => `\ PREFIX drawio: PREFIX object_diagram: SELECT DISTINCT ?diagramName ?cellId WHERE { <${cellIri}> drawio:hasDrawioId ?cellId . <${cellIri}> object_diagram:inModel ?model . ?model object_diagram:hasName ?diagramName . }`, // Query that gets ALL subclass relations. getSubClassRelations: ` PREFIX rdfs: SELECT DISTINCT ?subclass ?superclass WHERE { ?subclass rdfs:subClassOf ?superclass }`, getAllGraphs: ` SELECT ?g (COUNT(*) as ?count) {GRAPH ?g {?s ?p ?o}} GROUP BY ?g `, getArtifactFilename: artifactIri => `\ PREFIX pt: PREFIX pm: PREFIX base: SELECT DISTINCT ?filename ?formalismName WHERE { <${artifactIri}> pt:hasLocation ?filename . <${artifactIri}> pt:relatesTo ?pmArtifact . ?pmArtifact pm:hasType ?ftgFormalism . ?ftgFormalism base:hasGUID ?formalismName . }`, getModels: `\ PREFIX object_diagram: PREFIX rdfs: PREFIX cs_as: SELECT DISTINCT ?model ?type WHERE { ?model a object_diagram:Model . ?model a ?type . ?type rdfs:subClassOf object_diagram:Model . FILTER(NOT EXISTS { ?model a cs_as:CorrespondenceModel . }) FILTER(NOT EXISTS { ?more_concrete_type rdfs:subClassOf ?type . ?model a ?more_concrete_type . FILTER(?more_concrete_type != ?type) }) } `, getProperty: (iri, propertyIri) => `\ SELECT DISTINCT ?value WHERE { <${iri}> <${propertyIri}> ?value . } `, insertTraceLink: (fromIri, toIri) => `\ PREFIX trace: INSERT DATA { GRAPH { <${fromIri}> trace:traceLinkTo <${toIri}> . <${fromIri}> trace:traceLinkFrom <${toIri}> . } } `, }; // When rebuilding OML, all graphs will be deleted from Fuseki, except for the following (which are not sourced from OML, but inserted directly as RDF triples into Fuseki) GRAPHS_NOT_TO_DELETE = [ "http://ua.be/sdo2l/description/traces", ]; const typeToDescription = new Map([ ["ftg#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `ftg "${modelName}"`)], ["xopp#Text", async (element, type, getProperty) => getProperty("xopp#hasText").then(text => `text "${text}"`)], ["xopp#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `xournal++ model "${modelName}"`)], ["drawio#Cell", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `cell "${id}"`)], ["drawio#Vertex", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `vertex "${id}"`)], ["drawio#Edge", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `edge "${id}"`)], ["drawio#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `drawio model "${modelName}"`)], ["pm#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `process model "${modelName}"`)], ["pm#Activity", async (element, type, getProperty) => element], ["pm#Artifact", async (element, type, getProperty) => element], ["pm#CtrlInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl inport "${portname}"`)], ["pm#CtrlOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl outport "${portname}"`)], ["pm#DataInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data inport "${portname}"`)], ["pm#DataOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data outport "${portname}"`)], ]) // (Hardcoded) link types exposed to the user. const typeToLinkType = new Map([ ["object_diagram#Object", [ { relation: "cs_as#parsedAs", description: `Is parsed as`, }, { relation: "cs_as#renderedAs", description: `Is rendered as`, }, // Outcommented, because it's not an interesting relation: { relation: "object_diagram#inModel", description: `Is part of`, }, { relation: "traceability_model#traceLinkTo", description: `Trace-link (outgoing)`, }, { relation: "traceability_model#traceLinkFrom", description: `Trace-link (incoming)`, }, ]], ["drawio#Model", [ { relation: "drawio#hasRootCell", description: `Has root`, }, ]], // Outcommented, blows up: // ["drawio#Cell", [ // { // relation: "drawio#hasChild", // description: `Has child`, // }, // ]], ["pm#Activity", [ { relation: "pm#isTransformation", description: `Is typed by`, }, { relation: "pm#hasPort", description: `Has`, }, ]], ["pm#element", [ { relation: "processtraces#relatesTo", description: `Enacted by`, reverse: true, }, ]], ["processtraces#element", [ { relation: "processtraces#relatesTo", description: `Enactment of`, }, // pt-element is not a sub-type of 'Object', so we must repeat this: { relation: "cs_as#renderedAs", description: `Is rendered as`, }, ]], ["pm#Artifact", [ { relation: "pm#hasType", description: `Is typed by`, }, ]], ["ftg#Transformation", [ { relation: "pm#occursAsActivity", description: `Occurs as`, } ]], ["ftg#Formalism", [ { relation: "pm#occursAsArtifact", description: `Occurs as`, }, ]], ["xopp#Model", [ { relation: "xopp#hasPage", description: `Has page`, }, ]], ["xopp#Page", [ { relation: "xopp#hasLayer", description: `Has layer`, }, ]], ["xopp#Layer", [ { relation: "xopp#hasElement", description: `Has element`, }, ]], ]); // mapping from class-IRI to array of superclass-IRIs const superclasses = new Map(); // map is populated as soon as the plugin is loaded function getQueries(type) { const result = [].concat( typeToLinkType.get(dropVocabularyPrefix(type)) || [], ... (superclasses.get(type) || []).map(supertype => typeToLinkType.get(dropVocabularyPrefix(supertype)) || [])); console.log("getQueries,type=",type,"superclasses=",superclasses.get(type),"result=",result); return result; } function querySPARQL(query) { const body = new URLSearchParams(); body.append("query", query); return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, { headers: new Headers({"Content-Type": "application/x-www-form-urlencoded"}), method: "POST", body, }) .then(res => res.json()) .then(json => { console.log("Query:\n"+query+"\Result:",json.results.bindings); return json.results.bindings; }) } function updateSPARQL(update) { const body = new URLSearchParams(); body.append("update", update); return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, { headers: new Headers({"Content-Type": "application/x-www-form-urlencoded"}), method: "POST", body, }) .then(res => { console.log("Update:\n"+update+"\Result:",res); }) } const defaultSettings = { dialogPosX: 100, dialogPosY: 100, dialogWidth: 240, dialogHeight: 400, }; fetch(BACKEND+"/version") .then(response => response.json()) .catch(() => 0) // parsing failed - probably backend doesn't even have a version .then(version => { if (version !== EXPECTED_BACKEND_VERSION) { alert("Incorrect DTDesign Python backend version.\nExpected: " + EXPECTED_BACKEND_VERSION + ", got: " + version + ".\nRefusing to load plugin. Please upgrade :)"); return; } else { console.log("Backend version is ", version); console.log("All good.") } // Loads all 'rdfs:subClassOf' relations and stores them in a mapping. function loadSuperclasses() { return querySPARQL(QUERIES.getSubClassRelations) .then(results => { for (const {subclass, superclass} of results) { const superclasslist = superclasses.get(subclass.value) || (() => { const list = []; superclasses.set(subclass.value, list); return list; })(); superclasslist.push(superclass.value); } }); } loadSuperclasses(); // Override context menu when right-clicking somewhere in the diagram: const oldFactoryMethod = ui.editor.graph.popupMenuHandler.factoryMethod; ui.editor.graph.popupMenuHandler.factoryMethod = function(menu, cell, evt) { const modelverseMenu = menu.addItem("Knowledge Graph...", null, null); const createLinkMenu = menu.addItem("Create traceability link to...", null, null); menu.addSeparator(); oldFactoryMethod.apply(this, arguments); const entry = (element, type) => element+':'+type; function addMenuItem(element, type, maxRecursion, parentMenuItem, description, alreadyVisited, onClick) { if (alreadyVisited.has(entry(element, type))) { return; // don't go in circles } const shortType = dropVocabularyPrefix(type); const itemDescriptionGen = typeToDescription.get(shortType) || (async () => shortType); itemDescriptionGen(dropArtifactPrefix(element), shortType, property => { return querySPARQL(QUERIES.getProperty(element, addFormalismsPrefix(property))) .then(results => { if (results.length > 0) return results[0].value.value; else return ""; }); }) .then(itemDescription => { const menuItemText = description + ' ' + itemDescription; let createdMenuItem; // If the menu item is a (subtype of a) drawio cell, then clicking on it will take us to that cell in drawio. if (onClick) { createdMenuItem = menu.addItem(menuItemText, null, () => onClick(element), parentMenuItem); } else if ((superclasses.get(type) || []).some(t => t.endsWith("drawio#Cell"))) { createdMenuItem = menu.addItem(menuItemText, null, () => goto(element), parentMenuItem); } else { createdMenuItem = menu.addItem(menuItemText, null, null, parentMenuItem); } if ((superclasses.get(type) || []).some(t => t.endsWith("processtraces#Artifact"))) { querySPARQL(QUERIES.getArtifactFilename(element)) .then(results => { for (const {filename, formalismName} of results) { menu.addItem(`File "${filename.value}"`, null, () => (createDownloadHandler(formalismName.value))(filename.value), createdMenuItem); } }) } alreadyVisited.add(entry(element, type)); if (maxRecursion <= 0) { return; } addSubmenuItems(element, type, createdMenuItem, maxRecursion, alreadyVisited, onClick); }) } function addSubmenuItems(element, type, parentMenuItem, maxRecursion, alreadyVisited, onClick) { const queries = getQueries(type); for (const {relation, description, reverse} of queries) { querySPARQL(QUERIES.getOutgoingLink(element, "http://ua.be/sdo2l/vocabulary/formalisms/"+relation, reverse)) .then(results => { for (const {element, type} of results) { addMenuItem(element.value, type.value, maxRecursion-1, parentMenuItem, description, alreadyVisited, onClick); } }) } } // Populate ModelVerse submenu asynchronously: if (cell && cell.vertex) { querySPARQL(QUERIES.getCellStuff(cell.id)) .then(results => { for (const {element, type} of results) { const alreadyVisited = new Set([[element.value, type.value]]); addSubmenuItems(element.value, type.value, modelverseMenu, 3, alreadyVisited); querySPARQL(QUERIES.getModels) .then(results => { const alreadyVisited2 = new Set([[element.value, type.value]]); for (const {model, type} of results) { addMenuItem(model.value, type.value, 3, createLinkMenu, "Model", alreadyVisited2, targetIri => updateSPARQL(QUERIES.insertTraceLink(element.value, targetIri))); } }); } }) } } function getSetting(prop) { const result = localStorage.getItem("dtdesign-"+prop); if (result !== null) { return JSON.parse(result); } return defaultSettings[prop]; } function setSetting(prop, value) { localStorage.setItem("dtdesign-"+prop, JSON.stringify(value)); } const errWndDiv = document.createElement('div'); // wndDiv.style.color = "red"; errWndDiv.style.overflow = "auto"; errWndDiv.style.height = "100%"; errWndDiv.style.padding = '12px 14px 8px 14px'; const errWnd = new mxWindow("Error Details", errWndDiv, 300, 300, 300, 360, true, true); errWnd.destroyOnClose = false; errWnd.setMaximizable(false); errWnd.setResizable(true); errWnd.setClosable(true); function getErrDetailsAnchor() { const anchor = document.createElement('a'); anchor.innerText = "Details"; anchor.href = "#"; anchor.onclick = function() { errWnd.show(); }; return anchor; } let rebuildOMLPromise = Promise.resolve(); function rejectIfStatusAbove400(httpResponse) { if (httpResponse.status >= 400) { return httpResponse.text().then(text => { return Promise.reject(text); }); } return Promise.resolve(); } function rebuildOMLForced() { return querySPARQL(QUERIES.getAllGraphs) .then(results => { const graphsToDelete = results .map(({g,count}) => g.value) .filter(graphIri => !GRAPHS_NOT_TO_DELETE.includes(graphIri)); // Delete all graphs first return Promise.all(graphsToDelete.map(graphToDelete => fetch(SPARQL_SERVER+SPARQL_ENDPOINT+"?graph="+graphToDelete, { method: "DELETE", }) .then(rejectIfStatusAbove400) )) .then(() => { // Build OML and load the resulting .owl files: return fetch(BACKEND+"/owl_load", { method: "PUT", }) .then(rejectIfStatusAbove400) }); }); } function rebuildOMLIncremental() { // Backend takes care of all the heavy work: return fetch(BACKEND+"/owl_reload_incremental", { method: "PUT", }) .then(rejectIfStatusAbove400); } function showRebuildOMLResult(rebuildAsyncCallback) { // only perform one rebuild at a time -> queue rebuilds. rebuildOMLPromise = rebuildOMLPromise.then(() => { omlStatusDiv.style.color = null; omlStatusDiv.innerText = "Rebuilding OML..."; return rebuildAsyncCallback() .then(() => { omlStatusDiv.innerText = "✓ Built OML"; omlStatusDiv.style.color = "green"; }) .catch(errText => { errWndDiv.innerText = errText; omlStatusDiv.innerHTML = "✗ Failed to build OML "; omlStatusDiv.style.color = "red"; omlStatusDiv.appendChild(getErrDetailsAnchor()); }); }); } function uploadResponseHandler(statusDiv, refreshListCallback) { statusDiv.innerHTML = "Saving ..."; statusDiv.style.color = null; omlStatusDiv.innerHTML = ""; return res => { if (res.status >= 200 && res.status < 300) { errWndDiv.innerText = "No error."; res.json().then(parsedAs => { statusDiv.innerHTML = "✓ Generated OML (" + parsedAs.join(", ") + ")"; statusDiv.style.color = "green"; }); refreshListCallback(); showRebuildOMLResult(rebuildOMLIncremental); } else { statusDiv.innerHTML = "✗ Failed to save/parse. "; statusDiv.appendChild(getErrDetailsAnchor()); res.text().then(text => { errWndDiv.innerText = text; }); statusDiv.style.color = "red"; } }; } let highlight; // Given a cell IRI, open the diagram that contains the cell function goto(cellIri) { querySPARQL(QUERIES.getDiagramAndCellId(cellIri)) .then(([{diagramName, cellId}]) => { // drop the "_drawio" at the end: const actualDiagramName = diagramName.value.substring(0, diagramName.value.length-7); loadPage(actualDiagramName) .then(() => { if (highlight) { highlight.destroy(); } const [cell] = ui.editor.graph.getCellsById([cellId.value]); highlight = new mxCellHighlight(ui.editor.graph, "#eb34e8", 6); highlight.highlight(ui.editor.graph.view.getState(cell)); }) }) } const wndDiv = document.createElement('div'); wndDiv.style.userSelect = 'none'; wndDiv.style.height = '100%'; wndDiv.style.color = "rgb(112, 112, 112)"; wndDiv.style.overflow = 'auto'; wndDiv.classList.add("geFormatContainer"); const omlDiv = document.createElement('div'); omlDiv.classList.add('geFormatSection') omlDiv.style.padding = '12px 14px 8px 14px'; wndDiv.appendChild(omlDiv); const omlLabel = document.createElement('div'); omlLabel.innerText = "OML Status"; omlLabel.style.fontWeight = 'bold'; omlDiv.appendChild(omlLabel); const omlStatusDiv = document.createElement('div'); omlStatusDiv.innerText = "Ready"; omlDiv.appendChild(omlStatusDiv); const omlIncButton = mxUtils.button("Incremental rebuild (faster)", function() { showRebuildOMLResult(rebuildOMLIncremental); }); omlDiv.appendChild(omlIncButton); const omlForceButton = mxUtils.button("Forced rebuild (slow)", function() { showRebuildOMLResult(rebuildOMLForced); }); omlDiv.appendChild(omlForceButton); function createSavePageDiv(refreshModelsCallback) { const savePageDiv = document.createElement('div'); const saveButton = mxUtils.button("Save Current Page", function() { const responseHandler = uploadResponseHandler(saveStatusDiv, refreshModelsCallback); const headers = new Headers({ "Content-Type": "application/xml", }); const serializer = new XMLSerializer(); const xmlnode = ui.getXmlFileData( null, // ignore selection true, // only current page true); // uncompressed const diagram = xmlnode.children[0] const diagramName = diagram.getAttribute("name"); const xml = serializer.serializeToString(diagram); return fetch(BACKEND + "/files/drawio/"+diagramName, { method: "PUT", headers, body: xml, }) .then(responseHandler); }); saveButton.style.width = "100%"; saveButton.style.marginTop = "4px"; saveButton.style.marginBottom = "2px"; savePageDiv.appendChild(saveButton); const saveStatusDiv = document.createElement('div'); savePageDiv.appendChild(saveStatusDiv); return savePageDiv; } function createUploadDiv(modelType, mimeType) { return function(refreshModelsCallback) { const uploadDiv = document.createElement('div'); const uploadLabel = document.createElement('label'); uploadLabel.innerText = "Upload "+modelType+":"; uploadLabel.for = "uploadButton"; uploadDiv.appendChild(uploadLabel); const uploadButton = document.createElement('input'); uploadButton.type = "file"; uploadButton.id = "uploadButton"; uploadButton.style.width = "100%"; uploadButton.style.marginTop = "4px"; uploadButton.style.marginBottom = "2px"; uploadButton.accept = mimeType; // uploadButton.multiple = true; uploadButton.onchange = e => { const responseHandler = uploadResponseHandler(statusDiv, refreshModelsCallback); Array.from(e.target.files).map(file => { fetch(BACKEND+"/files/"+modelType+"/"+file.name, { method: "PUT", body: file, }) .then(responseHandler); }); }; uploadDiv.appendChild(uploadButton); const statusDiv = document.createElement('div'); uploadDiv.appendChild(statusDiv); return uploadDiv; }; } function createDownloadHandler(modelType) { return modelName => { const a = document.createElement('a'); document.body.appendChild(a); a.setAttribute('href', BACKEND + "/files/" + modelType + "/" + modelName); a.setAttribute('download', modelName); a.setAttribute('target', "_blank"); // for browsers that don't support the 'download' attribute, tell them to open the link in a new tab a.click(); document.body.removeChild(a); }; } function createModelList(modelType, createUploadDiv, downloadButtonLabel, onDownloadClick, extraStuff) { const containerDiv = document.createElement('div'); containerDiv.classList.add('geFormatSection') containerDiv.style.padding = '12px 14px 8px 14px'; wndDiv.appendChild(containerDiv); const labelDiv = document.createElement('div'); labelDiv.innerText = "Models — " + modelType; labelDiv.style.fontWeight = 'bold'; containerDiv.appendChild(labelDiv); const modelListDiv = document.createElement('div'); containerDiv.appendChild(modelListDiv); // Refreshes the list of models shown in the ModelVerse window function refreshList() { fetch(BACKEND + "/files/" + modelType, { method: "GET", }) .then(res => res.json()) .then(models => { if (models.length > 0) { modelListDiv.replaceChildren(...models.sort().map(modelName => { const div = document.createElement('div'); div.style.padding = '3px 0px'; const loadButton = mxUtils.button(downloadButtonLabel, () => onDownloadClick(modelName)); loadButton.style.marginLeft = "12px"; div.appendChild(document.createTextNode(modelName)); div.appendChild(loadButton); const extraStuffWrapper = document.createElement('div'); div.appendChild(extraStuffWrapper); function refreshExtraStuff() { extraStuff(modelName) .then(extraDiv => { if (extraDiv) { extraStuffWrapper.replaceChildren(extraDiv); extraDiv.appendChild(mxUtils.button("⟳", refreshExtraStuff)); } }); } refreshExtraStuff(); return div; })); } else { const div = document.createElement('div'); div.style.padding = '3px 0px'; div.appendChild(document.createTextNode("No "+modelType+" models.")); modelListDiv.replaceChildren(div); } }); } refreshList(); containerDiv.appendChild(createUploadDiv(refreshList)); return refreshList; } function createPTrenderCallback(startTraceIri) { return () => { return fetch(BACKEND+"/render_pt/"+encodeURIComponent(startTraceIri)) .then(treatResponseAsPageXml) .then(() => { refreshDrawioModelList(); showRebuildOMLResult(rebuildOMLIncremental); }); } } const extraPMstuff = modelName => { if (modelName.endsWith(":pm")) { const pmModel = addArtifactPrefix(modelName.substr(0, modelName.length-3)+"_pm#model"); const urlEncoded = encodeURIComponent(pmModel); const enactButton = mxUtils.button("Start New...", ()=>{ // open WEE in new tab: window.open(WEE+"/gettraces?iri="+urlEncoded, "_blank"); }); const shortTraceName = traceIri => dropArtifactPrefix(traceIri).split('#').findLast(str => str.length > 0); return Promise.all([ fetch(WEE+"/traces/active/"+urlEncoded) .then(response => response.json()) .then(enactments => { return enactments.map((enactment, i) => mxUtils.button(`Open "${shortTraceName(enactment.iri)}" (ongoing)`, createPTrenderCallback(enactment.iri))) }), fetch(WEE+"/traces/finished/"+urlEncoded) .then(response => response.json()) .then(enactments => { return enactments.map((enactment, i) => mxUtils.button(`Open "${shortTraceName(enactment.iri)}" (finished)`, createPTrenderCallback(enactment.iri))) }), ]) .then(([ongoing, finished]) => { const pmDiv = document.createElement('div'); pmDiv.style.marginLeft = '8px'; pmDiv.style.borderWidth = '1px'; pmDiv.style.borderStyle = 'solid'; pmDiv.appendChild(document.createTextNode("PM Enactment: ")); pmDiv.appendChild(enactButton); if (ongoing.length > 0) { ongoing.forEach(o => pmDiv.appendChild(o)); } if (finished.length > 0) { finished.forEach(f => pmDiv.appendChild(f)); } return pmDiv; }); } return Promise.resolve(null); }; const noExtraStuff = () => Promise.resolve(null); const refreshDrawioModelList = createModelList("drawio", createSavePageDiv, "Open", modelName => loadPage(modelName), extraPMstuff); createModelList("xopp", createUploadDiv("xopp", "application/x-xopp,.xopp"), "Download", createDownloadHandler("xopp"), noExtraStuff); createModelList("csv", createUploadDiv("csv", "text/csv,.csv"), "Download", createDownloadHandler("csv"), noExtraStuff); createModelList("file", createUploadDiv("file", ""), "Download", createDownloadHandler("file"), noExtraStuff); // Load a model and add it as a new page to the editor function treatResponseAsPageXml(response) { return response.text() .then(xmltext => { // console.log(xmltext); const parser = new DOMParser(); const doc = parser.parseFromString(xmltext, "application/xml"); const node = doc.documentElement; const page = new DiagramPage(node); // if page with same name already exists, erase it: ui.pages = ui.pages.filter(pg => pg.node.getAttribute("name") != page.node.getAttribute("name")); ui.pages.push(page); ui.currentPage = page; ui.editor.setGraphXml(node); }); } function loadPage(pageName) { return fetch(BACKEND + "/files/drawio/" + pageName) .then(treatResponseAsPageXml); } // make sure the plugin popup window is always within the bounds of the browser window: const dialogPosX = x => Math.max(0, Math.min(x, window.innerWidth - getSetting("dialogWidth"))); const dialogPosY = y => Math.max(0, Math.min(y, window.innerHeight - getSetting("dialogHeight"))); const wnd = new mxWindow("ModelVerse", wndDiv, dialogPosX(getSetting("dialogPosX")), dialogPosY(getSetting("dialogPosY")), getSetting("dialogWidth"), getSetting("dialogHeight"), true, true); wnd.destroyOnClose = false; wnd.setMaximizable(false); wnd.setResizable(true); wnd.setClosable(false); // remember window geometry in localstorage: wnd.addListener('resize', function(wnd) { // couldn't find a better way to get the new window size but the following: const parsepx = px => parseInt(px.substring(0, px.length-2)); setSetting("dialogWidth", parsepx(wnd.div.style.width)); setSetting("dialogHeight", parsepx(wnd.div.style.height)); }) wnd.addListener('move', function() { setSetting("dialogPosX", wnd.getX()); setSetting("dialogPosY", wnd.getY()); }) wnd.show(); window.onresize = event => { // make sure popup window remains within browser window bounds: const newX = dialogPosX(getSetting("dialogPosX")); const newY = dialogPosY(getSetting("dialogPosY")); wnd.div.style.left = newX + "px"; wnd.div.style.top = newY + "px"; // setSetting("dialogPosX", newX); // setSetting("dialogPosY", newY); } }) // end of promise that checks the backend version. })