|
@@ -1,5 +1,7 @@
|
|
|
Draw.loadPlugin(function(ui) {
|
|
|
|
|
|
+ const model = ui.editor.graph.model;
|
|
|
+
|
|
|
const defaultConfig = {
|
|
|
// When making an activity wider, the x-position of ports will be scaled proportionally to the new width, and vice-versa with height and y-position. This may be annoying when simply resizing an activity to add an extra port.
|
|
|
movePortsOnResizeActivity: false,
|
|
@@ -15,8 +17,8 @@ Draw.loadPlugin(function(ui) {
|
|
|
edgeStyleComment: "edgeStyle=0;endArrow=none;dashed=1;html=1;",
|
|
|
|
|
|
|
|
|
- portLabelOffsetEdgeDirection: 10,
|
|
|
- portLabelOffsetPerpendicularToEdgeDirection: 38,
|
|
|
+ portLabelOffsetEdgeDirection: 22,
|
|
|
+ portLabelOffsetPerpendicularToEdgeDirection: 24,
|
|
|
};
|
|
|
|
|
|
let currentConfig;
|
|
@@ -30,97 +32,149 @@ Draw.loadPlugin(function(ui) {
|
|
|
}
|
|
|
|
|
|
// Configuration dialog window:
|
|
|
-
|
|
|
- // Drawio has no decent abstraction for UI elements or even displaying a dialog window.
|
|
|
- // The following is based on code from src/main/webapp/js/diagramly/Dialogs.js
|
|
|
- const windowDiv = document.createElement('div');
|
|
|
- windowDiv.style.userSelect = 'none';
|
|
|
- windowDiv.style.overflow = 'hidden';
|
|
|
- windowDiv.style.padding = '10px';
|
|
|
- windowDiv.style.height = '100%';
|
|
|
-
|
|
|
- function createConfig(labelText, json, readonly) {
|
|
|
- const columnDiv = document.createElement('div');
|
|
|
- columnDiv.style.display = 'inline-block';
|
|
|
-
|
|
|
- const label = document.createElement('label');
|
|
|
- const textarea = document.createElement('textarea');
|
|
|
- label.setAttribute('for', textarea);
|
|
|
- label.innerHTML = labelText;
|
|
|
- textarea.setAttribute('wrap', 'off');
|
|
|
- textarea.setAttribute('spellcheck', 'false');
|
|
|
- textarea.setAttribute('autocorrect', 'off');
|
|
|
- textarea.setAttribute('autocomplete', 'off');
|
|
|
- textarea.setAttribute('autocapitalize', 'off');
|
|
|
- textarea.setAttribute('wrap', 'hard');
|
|
|
- if (readonly) {
|
|
|
- textarea.setAttribute('readonly', 'true');
|
|
|
+ function createFtgpmConfigWindow() { // only called once - this is only a function in order not to pollute namespace
|
|
|
+
|
|
|
+ // Drawio has no decent abstraction for UI elements or even displaying a dialog window.
|
|
|
+ // The following is based on code from src/main/webapp/js/diagramly/Dialogs.js
|
|
|
+ const wndDiv = document.createElement('div');
|
|
|
+ wndDiv.style.userSelect = 'none';
|
|
|
+ wndDiv.style.overflow = 'hidden';
|
|
|
+ wndDiv.style.padding = '10px';
|
|
|
+ wndDiv.style.height = '100%';
|
|
|
+ const wnd = new mxWindow("FTG+PM Plugin Configuration",
|
|
|
+ wndDiv, 100, 100, 640, 570, true, true);
|
|
|
+ wnd.destroyOnClose = false;
|
|
|
+ wnd.setMaximizable(false);
|
|
|
+ wnd.setResizable(false);
|
|
|
+ wnd.setClosable(true);
|
|
|
+ wnd.addListener('show', mxUtils.bind(this, function() {
|
|
|
+ statusText.innerHTML = "";
|
|
|
+ }));
|
|
|
+
|
|
|
+ function createConfig(labelText, json, readonly) {
|
|
|
+ const columnDiv = document.createElement('div');
|
|
|
+ columnDiv.style.display = 'inline-block';
|
|
|
+
|
|
|
+ const label = document.createElement('label');
|
|
|
+ const textarea = document.createElement('textarea');
|
|
|
+ label.setAttribute('for', textarea);
|
|
|
+ label.innerHTML = labelText;
|
|
|
+ textarea.setAttribute('wrap', 'off');
|
|
|
+ textarea.setAttribute('spellcheck', 'false');
|
|
|
+ textarea.setAttribute('autocorrect', 'off');
|
|
|
+ textarea.setAttribute('autocomplete', 'off');
|
|
|
+ textarea.setAttribute('autocapitalize', 'off');
|
|
|
+ textarea.setAttribute('wrap', 'hard');
|
|
|
+ if (readonly) {
|
|
|
+ textarea.setAttribute('readonly', 'true');
|
|
|
+ }
|
|
|
+ textarea.value = JSON.stringify(json, null, 2);
|
|
|
+ textarea.style.overflow = 'auto';
|
|
|
+ textarea.style.resize = 'none';
|
|
|
+ textarea.style.width = '300px';
|
|
|
+ textarea.style.height = '360px';
|
|
|
+ textarea.style.marginBottom = '16px';
|
|
|
+
|
|
|
+ columnDiv.appendChild(label);
|
|
|
+ mxUtils.br(columnDiv);
|
|
|
+ columnDiv.appendChild(textarea);
|
|
|
+ return [columnDiv, textarea];
|
|
|
}
|
|
|
- textarea.value = JSON.stringify(json, null, 2);
|
|
|
- textarea.style.overflow = 'auto';
|
|
|
- textarea.style.resize = 'none';
|
|
|
- textarea.style.width = '300px';
|
|
|
- textarea.style.height = '360px';
|
|
|
- textarea.style.marginBottom = '16px';
|
|
|
|
|
|
- columnDiv.appendChild(label);
|
|
|
- mxUtils.br(columnDiv);
|
|
|
- columnDiv.appendChild(textarea);
|
|
|
- return [columnDiv, textarea];
|
|
|
- }
|
|
|
-
|
|
|
- const [defaultColumn] = createConfig("Default config (hardcoded & read-only):", defaultConfig, true);
|
|
|
- const [customColumn, customTextArea] = createConfig("Custom config (acts as an 'overlay' on top of default config):", currentConfig, false);
|
|
|
- windowDiv.appendChild(defaultColumn);
|
|
|
- windowDiv.appendChild(customColumn);
|
|
|
-
|
|
|
- mxUtils.br(windowDiv);
|
|
|
-
|
|
|
- const statusText = document.createElement('div');
|
|
|
+ const [defaultColumn] = createConfig("Default config (hardcoded & read-only):", defaultConfig, true);
|
|
|
+ const [customColumn, customTextArea] = createConfig("Custom config (acts as an 'overlay' on top of default config):", currentConfig, false);
|
|
|
+ wndDiv.appendChild(defaultColumn);
|
|
|
+ wndDiv.appendChild(customColumn);
|
|
|
+
|
|
|
+ mxUtils.br(wndDiv);
|
|
|
+
|
|
|
+ const buttonsDiv = document.createElement('div');
|
|
|
+ buttonsDiv.style.textAlign = 'right';
|
|
|
+ const statusText = document.createElement('div');
|
|
|
+ statusText.style.display = 'inline-block';
|
|
|
+
|
|
|
+ // attempts to save user configuration and displays status text as a side effect
|
|
|
+ // returns true if all went well
|
|
|
+ function saveConfigurationAndDisplayStatus() {
|
|
|
+ let parsed;
|
|
|
+ try {
|
|
|
+ parsed = JSON.parse(customTextArea.value); // may throw
|
|
|
+ } catch (parseErr) {
|
|
|
+ statusText.innerHTML = "Parse error: " + parseErr.toString();
|
|
|
+ statusText.style.color = 'red';
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
|
|
|
+ statusText.innerHTML = "JSON value is not an object";
|
|
|
+ statusText.style.color = 'red';
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ currentConfig = parsed;
|
|
|
+ window.localStorage.setItem('ftgpmConfig', JSON.stringify(parsed));
|
|
|
+ customTextArea.value = JSON.stringify(parsed, null, 2); // prettify
|
|
|
+ statusText.innerHTML = "Configuration successfully set and saved (in window.localStorage)";
|
|
|
+ statusText.style.color = 'green';
|
|
|
+ return true;
|
|
|
+ }
|
|
|
|
|
|
- const buttonsDiv = document.createElement('div');
|
|
|
+ const applyButton = mxUtils.button("Apply", saveConfigurationAndDisplayStatus);
|
|
|
+ applyButton.className = 'geBtn gePrimaryBtn';
|
|
|
+ applyButton.style.float = 'none'; // override geBtn style
|
|
|
|
|
|
- const applyButton = mxUtils.button("Apply", function() {
|
|
|
- let parsed;
|
|
|
- try {
|
|
|
- parsed = JSON.parse(customTextArea.value); // may throw
|
|
|
- } catch (parseErr) {
|
|
|
- statusText.innerHTML = "Parse error: " + parseErr.toString();
|
|
|
- return;
|
|
|
- }
|
|
|
- if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
|
|
|
- statusText.innerHTML = "JSON value is not an object";
|
|
|
- return;
|
|
|
- }
|
|
|
- currentConfig = parsed;
|
|
|
- window.localStorage.setItem('ftgpmConfig', JSON.stringify(parsed));
|
|
|
- customTextArea.value = JSON.stringify(parsed, null, 2); // prettify
|
|
|
- statusText.innerHTML = "Configuration successfully set and saved (in window.localStorage)"
|
|
|
- });
|
|
|
- applyButton.className = 'geBtn gePrimaryBtn';
|
|
|
- applyButton.style.float = 'right';
|
|
|
+ const resetButton = mxUtils.button("Reset", function() {
|
|
|
+ customTextArea.value = JSON.stringify(currentConfig, null, 2);
|
|
|
+ });
|
|
|
+ resetButton.className = 'geBtn';
|
|
|
+ resetButton.style.float = 'none'; // override geBtn style
|
|
|
+
|
|
|
+ buttonsDiv.appendChild(statusText);
|
|
|
+ buttonsDiv.appendChild(applyButton);
|
|
|
+ buttonsDiv.appendChild(resetButton);
|
|
|
+
|
|
|
+ // hacks buttons:
|
|
|
+ const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
|
|
|
+ if (saveConfigurationAndDisplayStatus()) {
|
|
|
+ model.beginUpdate();
|
|
|
+ for (const [cellId, cell] of Object.entries(model.cells)) {
|
|
|
+ applyCellStyleFromConfig(cell);
|
|
|
+ }
|
|
|
+ model.endUpdate();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
|
|
|
+ if (saveConfigurationAndDisplayStatus()) {
|
|
|
+ model.beginUpdate();
|
|
|
+ for (const [cellId, cell] of Object.entries(model.cells)) {
|
|
|
+ if (cell.geometry) {
|
|
|
+ // we'll force a geometry update:
|
|
|
+ const newGeometry = cell.geometry.clone();
|
|
|
+ snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
|
|
|
+ model.setGeometry(cell, newGeometry);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ model.endUpdate();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ forceEdgeStyleButton.className = 'geBtn';
|
|
|
+ forceEdgeStyleButton.style.float = 'none'; // override geBtn style
|
|
|
+ forcePortSnapButton.className = 'geBtn';
|
|
|
+ forcePortSnapButton.style.float = 'none'; // override geBtn style
|
|
|
|
|
|
- const resetButton = mxUtils.button("Reset", function() {
|
|
|
- customTextArea.value = JSON.stringify(currentConfig, null, 2);
|
|
|
- });
|
|
|
- resetButton.className = 'geBtn';
|
|
|
- resetButton.style.float = 'right';
|
|
|
+ hacksDiv = document.createElement('div');
|
|
|
+ hacksDiv.appendChild(document.createTextNode("Hacks: (use undo if you fuck things up)"));
|
|
|
+ mxUtils.br(hacksDiv);
|
|
|
+ hacksDiv.appendChild(forceEdgeStyleButton);
|
|
|
+ mxUtils.br(hacksDiv);
|
|
|
+ hacksDiv.appendChild(forcePortSnapButton);
|
|
|
|
|
|
- buttonsDiv.appendChild(applyButton);
|
|
|
- buttonsDiv.appendChild(resetButton);
|
|
|
- buttonsDiv.appendChild(statusText);
|
|
|
- windowDiv.appendChild(buttonsDiv);
|
|
|
+ wndDiv.appendChild(buttonsDiv);
|
|
|
+ mxUtils.br(wndDiv);
|
|
|
+ wndDiv.appendChild(hacksDiv);
|
|
|
|
|
|
- const ftgpmConfigWindow = new mxWindow("FTG+PM Plugin Configuration",
|
|
|
- windowDiv, 100, 100, 640, 480, true, true);
|
|
|
- ftgpmConfigWindow.destroyOnClose = false;
|
|
|
- ftgpmConfigWindow.setMaximizable(false);
|
|
|
- ftgpmConfigWindow.setResizable(false);
|
|
|
- ftgpmConfigWindow.setClosable(true);
|
|
|
+ return wnd;
|
|
|
+ }
|
|
|
|
|
|
- ftgpmConfigWindow.addListener('show', mxUtils.bind(this, function() {
|
|
|
- statusText.innerHTML = "";
|
|
|
- }));
|
|
|
+ const ftgpmConfigWindow = createFtgpmConfigWindow();
|
|
|
|
|
|
function getFromConfig(parameter) {
|
|
|
if (currentConfig.hasOwnProperty(parameter)) {
|
|
@@ -130,19 +184,47 @@ Draw.loadPlugin(function(ui) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const model = ui.editor.graph.model;
|
|
|
+
|
|
|
+ // // Force style window:
|
|
|
+ // const forceStyleDiv = document.createElement('div');
|
|
|
+
|
|
|
+ // const forceStyleWindow = new mxWindow("FTG+PM: Force Style Rules",
|
|
|
+ // forceStyleDiv, 200, 200, 400, 90, true, true);
|
|
|
+ // forceStyleWindow.destroyOnClose = false;
|
|
|
+ // forceStyleWindow.setMaximizable(false);
|
|
|
+ // forceStyleWindow.setResizable(false);
|
|
|
+ // forceStyleWindow.setClosable(true);
|
|
|
+
|
|
|
+ // const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
|
|
|
+ // for (const [cellId, cell] of Object.entries(model.cells)) {
|
|
|
+ // applyCellStyleFromConfig(cell);
|
|
|
+ // }
|
|
|
+ // });
|
|
|
+ // const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
|
|
|
+ // for (const [cellId, cell] of Object.entries(model.cells)) {
|
|
|
+ // if (cell.geometry) {
|
|
|
+ // // we'll force a geometry update:
|
|
|
+ // const newGeometry = cell.geometry.clone();
|
|
|
+ // snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
|
|
|
+ // model.setGeometry(cell, newGeometry);
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ // })
|
|
|
+ // forceEdgeStyleButton.className = 'geBtn';
|
|
|
+ // forcePortSnapButton.className = 'geBtn';
|
|
|
+ // forceStyleDiv.appendChild(forceEdgeStyleButton);
|
|
|
+ // mxUtils.br(forceStyleDiv);
|
|
|
+ // forceStyleDiv.appendChild(forcePortSnapButton);
|
|
|
|
|
|
window.onkeyup = function(e) {
|
|
|
// Shortcut to show FTG+PM config:
|
|
|
if (e.ctrlKey && e.key === '.') {
|
|
|
ftgpmConfigWindow.show();
|
|
|
}
|
|
|
- // Shortcut to apply style from config to all edges:
|
|
|
- if (e.ctrlKey && e.key === 'Enter') {
|
|
|
- for (const [cellId, cell] of Object.entries(model.cells)) {
|
|
|
- applyCellStyleFromConfig(cell);
|
|
|
- }
|
|
|
- }
|
|
|
+ // // Shortcut to apply style from config to all edges:
|
|
|
+ // if (e.ctrlKey && e.key === 'Enter') {
|
|
|
+ // forceStyleWindow.show();
|
|
|
+ // }
|
|
|
}
|
|
|
|
|
|
|
|
@@ -590,7 +672,7 @@ Draw.loadPlugin(function(ui) {
|
|
|
}
|
|
|
|
|
|
// Update port shape (so it points in the right direction), move port (and port label) to border of activity.
|
|
|
- function moveToBorder(cell, cellGeometry, parentGeometry, border) {
|
|
|
+ function snapPortToBorder(cell, cellGeometry, parentGeometry, border) {
|
|
|
const style = parseStyle(model.getStyle(cell));
|
|
|
const type = cell.getAttribute(TYPE_ATTR);
|
|
|
const inOrOut = isInport(type) ? "in" : "out";
|
|
@@ -650,23 +732,27 @@ Draw.loadPlugin(function(ui) {
|
|
|
model.setStyle(cell, unparseStyle(style));
|
|
|
}
|
|
|
|
|
|
+ function snapToBorderIfCellIsPortAndParentIsActivity(cell, geometry) {
|
|
|
+ const type = cell.getAttribute(TYPE_ATTR);
|
|
|
+ if (isPort(type)) {
|
|
|
+ // Port was moved
|
|
|
+ if (cell.parent) {
|
|
|
+ const parentType = cell.parent.getAttribute(TYPE_ATTR);
|
|
|
+ if (isActivityNode(parentType) || isTransformation(parentType)) {
|
|
|
+ // Snap port to activity border
|
|
|
+ const portSnappingAlgorithm = getFromConfig('newPortSnappingAlgorithm') ? closestBorder2 : closestBorder;
|
|
|
+ // border will be one of: 'l', 'r', 't', 'b'
|
|
|
+ const border = portSnappingAlgorithm(geometry, cell.parent.geometry);
|
|
|
+ snapPortToBorder(cell, geometry, cell.parent.geometry, border);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
ui.editor.graph.addListener(mxEvent.MOVE_CELLS, (_, eventObj) => {
|
|
|
// High-level event: Happens when the user releases dragged shape(s)
|
|
|
for (const cell of eventObj.properties.cells) {
|
|
|
- const type = cell.getAttribute(TYPE_ATTR);
|
|
|
- if (isPort(type)) {
|
|
|
- // Port was moved
|
|
|
- if (cell.parent) {
|
|
|
- const parentType = cell.parent.getAttribute(TYPE_ATTR);
|
|
|
- if (isActivityNode(parentType) || isTransformation(parentType)) {
|
|
|
- // Snap port to activity border
|
|
|
- const portSnappingAlgorithm = getFromConfig('newPortSnappingAlgorithm') ? closestBorder2 : closestBorder;
|
|
|
- // border will be one of: 'l', 'r', 't', 'b'
|
|
|
- const border = portSnappingAlgorithm(cell.geometry, cell.parent.geometry);
|
|
|
- moveToBorder(cell, cell.geometry, cell.parent.geometry, border);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ snapToBorderIfCellIsPortAndParentIsActivity(cell, cell.geometry);
|
|
|
}
|
|
|
});
|
|
|
|
|
@@ -702,9 +788,9 @@ Draw.loadPlugin(function(ui) {
|
|
|
}
|
|
|
else if (isPort(childType)) {
|
|
|
// Move contained ports
|
|
|
- const border = closestBorder2(child.geometry, prevGeometry);
|
|
|
+ const border = closestBorder2(child.geometry, prevGeometry); // < what was the border in the old geometry?
|
|
|
const newGeometry = child.geometry.clone();
|
|
|
- moveToBorder(child, newGeometry, cell.geometry, border);
|
|
|
+ snapPortToBorder(child, newGeometry, cell.geometry, border);
|
|
|
if (getFromConfig('movePortsOnResizeActivity')) {
|
|
|
// Scale position
|
|
|
switch (border) {
|
|
@@ -722,7 +808,7 @@ Draw.loadPlugin(function(ui) {
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
- ui.editor.graph.model.setGeometry(child, newGeometry);
|
|
|
+ model.setGeometry(child, newGeometry);
|
|
|
}
|
|
|
}
|
|
|
}
|