So the solution that I settled on was this:
The XML I tried to define was this:
// In my app.js I had:
// import urlRefPackage from './moddleExtensions/urlRefPackage.json'
// which is this: urlRefPackage.json
{
"name": "UrlReference",
"uri": "http://url_reference",
"prefix": "url",
"xml": {
},
"types": [
{
"name": "ref",
"superClass": [ "Element" ],
"properties": [
{
"name": "location",
"isAttr": true,
"type": "String"
}
]
}
],
"emumerations": [],
"associations": []
}
Now this defined the item that will get stuck into the XML as party of a “bpmn2:extensionElements”. Example is this:
// ... more xml
<bpmn2:startEvent id="StartEvent_1">
<bpmn2:extensionElements>
<url:ref location="https://www.google.com/" />
</bpmn2:extensionElements>
<bpmn2:outgoing>SequenceFlow_1a2fxeb</bpmn2:outgoing>
</bpmn2:startEvent>
// ... more xml
Which when loaded in I utilize with this function:
modeler.on('element.click', function(event) {
// console.log("modeler on element click");
let element = event.element,
moddle = modeler.get('moddle'),
// the underlaying BPMN 2.0 element
businessObject = element.businessObject,
analysis,
score,
message,
url_ref;
function getExtension(element, type) {
if (!element.extensionElements) {
return null;
}
// console.log(element.extensionElements);
return element.extensionElements.values.filter(function(e) {
return e.$instanceOf(type);
})[0];
}
url_ref = getExtension(businessObject, 'url:ref');
if (url_ref) {
// console.log(url_ref);
if (typeof url_ref.location == "string") {
if (validUrl(url_ref.location)) {
if (confirm("Open related link?")) {
window.open(url_ref.location, "_blank");
}
} else {
console.warn("url:ref location failed valid url check.");
}
}
}
});
So when I click on an element that has that data defined then it goes in and gets the LOCATION which is a valid URL (hopefully), and then it performed the “window.open(url_ref.location, “_blank”);” and open up the URL in a new tab.
Now the code is not yet present to handle this, but I am planning on adding code when the modeler opens where it will look for hash additions to the URL which will contain info on the DB file to loaded. It would look like:
https://reporting.dev.gcumedia.com/mediaElements/process-notation-tool/v1.1/#/x5j2rd
Where the code “x5j2rd” in the URL is going to be related to a XML data lump that is on a MS SQL database, and gets loaded in by my already existing functions that loads the data from my SQL database server. It calls to an API written in PHP which works as my back end to communicate with SQL and then delivers back the data as a JSON object that has the xml data in it as a base64 encode of a uri encoded xml output (and thus decoded in the same order… See wall of code below to hunt down that process in there).
So the element can be clicked on and work more or less like HTML A tags which opens a new window or tab with the URL to the modeler with a reference to a different XML document/diagram which is loaded up from the Database server back end I have set up.
Now one may not that I don’t yet have something defined to add in this data to a element/object in the diagram yet. I have some commented out code in the wall of code that would just add a static data url item to anything I clicked on. I will be making a more intelligent interface for the add, update, and removal of that data in due time. I just wanted to report what solutions I have come up with so far in case it sparks and creative development ideas for the project as a whole. (If the development team what to come up with a better solution for a more permanent feature that allows the type of functionality.)
So thank you everyone in this thread for the help and input. It was a great learning experience that has really helped me to understand the code under the hood for the BPMN project a bit better. If you have any suggestions or comments, then I would be quite open to them. If there are chunks of code referenced, but you don’t see, then feel free to ask for them and I will provide what ever I may have forgotten to place in this post. I am trying to give back as much to the community of what I learned in hopes of helping everyone else out as well. Thank you
Wall of Code:
import diagramStyle from 'bpmn-js/dist/assets/diagram-js.css'; // eslint-disable-line no-unused-vars
import bpmnStyle from 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
import icons from 'bpmn-font/dist/css/bpmn-embedded.css'; // eslint-disable-line no-unused-vars
// import commentsStyle from 'bpmn-js-embedded-comments/assets/comments.css';
import commentsStyle from './custom-embedded-comments/comments.css';
import propertiesStyle from 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css';
import appStyle from './css/app.css';
// Yes this is technically loaded twice, but but the load in html makes it easier to find where the css items are to edit and debug.
// Since it is loaded here though that means the app.css is technically the last thing to set any CSS rule thus I can more easily override
// classes and rules set in the other CSS files previously loaded.
import $ from 'jquery';
// import CustomModeler from './custom-modeler'; // This already calls BpmnModeler inside of itself.
// import CustomModeler from './custom-modeler-reorganized'; // This already calls BpmnModeler inside of itself.
import BpmnModeler from 'bpmn-js/lib/Modeler';
import diagramXML from '../resources/newDiagram.bpmn';
// import EmbeddedComments from 'bpmn-js-embedded-comments';
import EmbeddedComments from './custom-embedded-comments';
// import nyanObjectModule from './custom-modeler-reorganized/nyan';
import propertiesPanelModule from 'bpmn-js-properties-panel';
import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/bpmn';
import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda';
import qaPackage from './moddleExtensions/qaPackage.json'
import urlRefPackage from './moddleExtensions/urlRefPackage.json'
import colorPickerModule from './color-picker';
import { b64EncodeUnicode, b64DecodeUnicode} from './functions/b64_unicode.js';
import { apiGet, apiPost } from './functions/apiGetPost.js';
var container = $('#js-drop-zone');
// window.modeler = new CustomModeler({
window.modeler = new BpmnModeler({
container: '#js-canvas',
propertiesPanel: {
parent: '#js-properties'
},
additionalModules: [
// nyanObjectModule,
colorPickerModule,
EmbeddedComments,
propertiesPanelModule,
propertiesProviderModule
],
moddleExtensions: {
camunda: camundaModdleDescriptor,
qa: qaPackage,
urlRef: urlRefPackage
},
keyboard: {
bindTo: document
}
});
window.modelerEventBus = modeler.get('eventBus');
window.modelerChanged = false;
window.currentProject = { username: "", foldername: "", filename: "" };
window.file_set_username = file_set_username;
function file_set_username(newName, event) {
// console.log("file_set_username: "+newName);
// console.log(event);
window.currentProject.username = newName;
document.getElementById("leftSidebar-username").value = newName;
}
window.file_set_foldername = file_set_foldername;
function file_set_foldername(newName, event) {
// console.log("file_set_foldername: "+newName);
// console.log(event);
window.currentProject.foldername = newName;
document.getElementById("leftSidebar-foldername").value = newName;
}
window.file_set_filename = file_set_filename;
function file_set_filename(newName, event) {
// console.log("file_set_filename: "+newName);
// console.log(event);
window.currentProject.filename = newName;
document.getElementById("leftSidebar-filename").value = newName;
if (modeler._definitions) { modeler._definitions.$attrs['diagram-name'] = newName; }
let diagramName = document.getElementById("diagramName")
diagramName.value = newName;
diagramName.size = newName.trim().length;
}
window.file_clean_up = file_clean_up;
function file_clean_up() {
file_set_username(window.currentProject.username.trim());
file_set_foldername(window.currentProject.foldername.trim());
file_set_filename(window.currentProject.filename.trim());
}
function createNewDiagram() {
openDiagram(diagramXML);
}
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}
function validUrl(url_string){
let elm = document.createElement('input');
elm.setAttribute('type', 'url');
elm.value = url_string;
return elm.validity.valid;
}
// Possible events: element.click, element.dblclick, ?
modeler.on('element.click', function(event) {
// console.log("modeler on element click");
let element = event.element,
moddle = modeler.get('moddle'),
// the underlaying BPMN 2.0 element
businessObject = element.businessObject,
analysis,
score,
message,
url_ref;
function getExtension(element, type) {
if (!element.extensionElements) {
return null;
}
// console.log(element.extensionElements);
return element.extensionElements.values.filter(function(e) {
return e.$instanceOf(type);
})[0];
}
url_ref = getExtension(businessObject, 'url:ref');
if (url_ref) {
// console.log(url_ref);
if (typeof url_ref.location == "string") {
if (validUrl(url_ref.location)) {
if (confirm("Open related link?")) {
window.open(url_ref.location, "_blank");
}
} else {
console.warn("url:ref location failed valid url check.");
}
}
}
// analysis = getExtension(businessObject, 'qa:AnalysisDetails');
// console.log(analysis);
// if (analysis) {
// score = businessObject.suitable;
// if (isNaN(score)) {
// message = 'No suitability score yet, dblclick to assign one';
// } else {
// message = 'Diagram element has a suitability score of ' + score;
// }
// if (analysis) {
// message += '\n Last analyzed at ' + analysis.lastChecked;
// }
// window.alert(message);
// }
// url_ref = getExtension(businessObject, 'url:ref');
// if (!url_ref) {
// url_ref = moddle.create('url:ref');
// if (!businessObject.extensionElements) {
// businessObject.extensionElements = moddle.create('bpmn:ExtensionElements');
// }
// url_ref.location = "https://www.google.com/";
// businessObject.extensionElements.get('values').push(url_ref);
// console.log(businessObject);
// }
});
modeler.on('import.parse.complete', ({ error, definitions, context }) => {
// manipulate definitions before import
if(definitions.id === "sample-diagram") definitions.id = guid();
// console.log(definitions);
// document.querySelector('#diagramName').innerText = definitions.$attrs['diagram-name'] || "Diagram Name";
let newName = definitions.$attrs['diagram-name'] || "Diagram Name";
file_set_filename(newName);
file_clean_up();
// make sure to return the manipulated defintions
return definitions;
});
// This takes care of updates and changes to the name of the document. I.E. the file name.
// document.getElementById("diagramName").addEventListener("input", updateFileName, false);
// function updateFileName(event) { modeler._definitions.$attrs['diagram-name'] = document.querySelector('#diagramName').innerText; }
function openDiagram(xml) {
modeler.importXML(xml, function(err) {
if (err) {
container
.removeClass('with-diagram')
.addClass('with-error');
container.find('.error pre').text(err.message);
console.error(err);
} else {
container
.removeClass('with-error')
.addClass('with-diagram');
$('.show-with-diagram').addClass('shown-with-diagram').removeClass('show-with-diagram');
}
modeler.on('saveXML.start', ({ definitions }) => {
definitions.$attrs['diagram-name'] = window.currentProject.filename.trim();
return definitions;
});
modeler.on('saveXML.serialized', ({ xml }) => {
//this is where we will send it to the database to save
//console.log(xml);
return xml;
});
modeler.get('canvas').zoom('fit-viewport');
});
}
function saveSVG(done) {
modeler.saveSVG(done);
}
function saveDiagram(done) {
modeler.saveXML({ format: true }, function(err, xml) {
done(err, xml);
});
}
function loadFromDB(event) {
let prompt_filename = window.prompt("Filename to load:");
prompt_filename = prompt_filename.trim();
if (prompt_filename === "") return;
let data_id = {
"csrfToken": csrfToken,
"bpmn_username": window.currentProject.username.trim() || "jeremy.mone",
"bpmn_foldername": window.currentProject.foldername.trim() || "testFolder2",
"bpmn_filename": prompt_filename.trim()
};
apiPost("api/api.php?args=/bpmn_load/true", JSON.stringify(data_id),
function(result){ // Success
// console.log('success!');
// console.log(result);
let bpmn_json = JSON.parse(result.responseText);
// console.log(bpmn_json);
// console.log(bpmn_json.results[0].data.length);
// console.log(bpmn_json.results[0].data);
// console.log(atob(bpmn_json.results[0].data));
// let loaded_xml = decodeURIComponent(atob(bpmn_json.results[0].data));
let loaded_xml = b64DecodeUnicode(bpmn_json.results[0].data);
// console.log(loaded_xml);
openDiagram(loaded_xml);
file_set_username(bpmn_json.results[0].username);
file_set_foldername(bpmn_json.results[0].foldername);
file_set_filename(bpmn_json.results[0].filename);
file_clean_up();
},
function(result){ // Fail
console.log('The request failed!');
console.log(xhr);
}
);
}
// Added a button in html and this is the setting up and function for the click action of the button to save to db.
function saveToDB(event) {
// console.log("saveToDB", event);
modeler.saveXML({ format: true }, function(err, xml) {
if (err) { console.log(err); } // We got an error so we show an error.
else if (xml) { // We got data back. Assuming this is good.
// var bpmn_base64_xml = btoa(encodeURIComponent(xml));
var bpmn_base64_xml = b64EncodeUnicode(xml);
// console.log(bpmn_base64_xml.length);
file_clean_up();
let data_output = {
"csrfToken": csrfToken,
"bpmn_username": window.currentProject.username.trim() || "jeremy.mone",
"bpmn_foldername": window.currentProject.foldername.trim() || "testFolder2",
"bpmn_filename": window.currentProject.filename.trim(),
"bpmn_data": bpmn_base64_xml
};
// apiGet("api/api.php?args=/bpmn_load/true", function(result){
// console.log('success!', result);
// let bpmn_json = JSON.parse(result.responseText);
// console.log(bpmn_json);
// },
// function(result){
// console.log('The request failed!');
// console.log(xhr);
// });
apiPost("api/api.php?args=/bpmn_update/true", JSON.stringify(data_output),
function(result){ // Success
// console.log('success!');
// console.log(result);
let bpmn_json = JSON.parse(result.responseText);
// console.log(bpmn_json);
},
function(result){ // Fail
console.log('The request failed!');
console.log(xhr);
}
);
} else { // We got nothing on either err or xml. No idea what would do this, but noting something wrong.
console.error("saveToDB - modeler.saveXML: err and xml both empty or invalid.");
}
});
}
function registerFileDrop(container, callback) {
function handleFileSelect(e) {
e.stopPropagation();
e.preventDefault();
var files = e.dataTransfer.files;
var file = files[0];
var reader = new FileReader();
reader.onload = function(e) {
var xml = e.target.result;
callback(xml);
};
reader.readAsText(file);
}
function handleDragOver(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
}
container.get(0).addEventListener('dragover', handleDragOver, false);
container.get(0).addEventListener('drop', handleFileSelect, false);
}
// file drag / drop ///////////////////////
// check file api availability
if (!window.FileList || !window.FileReader) {
window.alert(
'Looks like you use an older browser that does not support drag and drop. ' +
'Try using Chrome, Firefox or the Internet Explorer > 10.');
} else {
registerFileDrop(container, openDiagram);
}
// bootstrap diagram functions
$(function() {
$('#js-create-diagram').click(function(e) {
e.stopPropagation();
e.preventDefault();
createNewDiagram();
});
var downloadLink = $('#js-download-diagram');
var downloadSvgLink = $('#js-download-svg');
$('.buttons a').click(function(e) {
if (!$(this).is('.active')) {
e.preventDefault();
e.stopPropagation();
}
});
function setEncoded(link, name, data) {
var encodedData = encodeURIComponent(data);
if (data) {
link.addClass('active').attr({
'href': 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData,
'download': name.replace(/[\^\/:\*\?"<>\|]/, '-').replace(/ /g, '_')
});
} else {
link.removeClass('active');
}
}
var exportArtifacts = debounce(function() {
// console.log("exportArtifacts called");
modelerChanged = true;
saveSVG(function(err, svg) {
setEncoded(downloadSvgLink, `${window.currentProject.filename.trim()}.svg`, err ? null : svg);
});
saveDiagram(function(err, xml) {
setEncoded(downloadLink, `${window.currentProject.filename.trim()}.bpmn`, err ? null : xml);
});
}, 500);
$("#js-load-from-db").on("click", loadFromDB);
$("#js-save-to-db").on("click", saveToDB);
// This hopefully captures the proper data for things that may not naturally trigger the commandStack.changed event.
$("#js-download-diagram").on("mouseenter", function() {
if (modelerChanged) { exportArtifacts(); }
// console.log("#js-download-diagram mouseenter");
});
$("#js-download-svg").on("mouseenter", function() {
if (modelerChanged) { exportArtifacts(); }
// console.log("#js-download-svg mouseenter");
});
modeler.on('comments.updated', exportArtifacts);
modeler.on('commandStack.changed', exportArtifacts);
modelerEventBus.on("element.click", function(e) {
// e.element = the model element
// e.gfx = the graphical element
// console.log("element.click on", e);
modeler.get('comments').collapseAll();
});
});
// helpers //////////////////////
function debounce(fn, timeout) {
var timer;
return function() {
if (timer) { clearTimeout(timer); }
timer = setTimeout(fn, timeout);
};
}
color-picker
import ColorPicker from './ColorPicker';
//import ColoredRenderer from './ColoredRenderer';
export default {
//__init__: [ 'colorPicker', 'coloredRenderer' ],
__init__: [ 'colorPicker' ],
colorPicker: [ 'type', ColorPicker ],
//coloredRenderer: [ 'type', ColoredRenderer ]
};
import { is } from 'bpmn-js/lib/util/ModelUtil';
import chromaJs from 'chroma-js'; // More info at: https://github.com/gka/chroma.js/
/**
* A basic color picker implementation.
*
* @param {EventBus} eventBus
* @param {ContextPad} contextPad
* @param {CommandStack} commandStack
*/
export default function ColorPicker(eventBus, contextPad, commandStack) {
contextPad.registerProvider(this);
commandStack.registerHandler('shape.updateColor', UpdateColorHandler);
function changeColor(event, element) {
// let selectedColor = window.prompt('type a color code');
//commandStack.execute('shape.updateColor', { element: element, color: color });
let selectedColor = "#000000";
let generalColorPickerElement = document.getElementById("generalColorPicker");
function handleColorPickerEvent(event){
selectedColor = event.target.value;
let color = chromaJs(selectedColor).darken(1).hex();
let bgColor = chromaJs(selectedColor).brighten().hex();
let modelThing = modeler.get('modeling');
let modelElements = [];
modelElements.push(element)
modelThing.setColor(modelElements, {
stroke: color,
fill: bgColor
});
generalColorPickerElement.removeEventListener("change", handleColorPickerEvent, false);
}
generalColorPickerElement.addEventListener("change", handleColorPickerEvent, false);
generalColorPickerElement.click(); // We force a click on the hidden input element to open the color picker window.
}
this.getContextPadEntries = function(element) {
//if (is(element, 'bpmn:Event')) {
return {
'changeColor': {
group: 'edit',
className: 'bpmn-icon-color-picker',
title: 'Change element color',
action: {
click: changeColor
}
}
};
//}
};
}
/**
* A handler updating an elements color.
*/
function UpdateColorHandler() {
this.execute = function(context) {
context.oldColor = context.element.color;
context.element.color = context.color;
return context.element;
};
this.revert = function(context) {
context.element.color = context.oldColor;
return context.element;
};
}
apiGetPost.js
export function apiGet(url, success, fail) {
let xhr = new XMLHttpRequest();
xhr.onload = function() {
// Process our return data
if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 300) {
// This will run when the request is successful
success(xhr);
} else {
// This will run when it's not
fail(xhr);
}
}
xhr.open("GET", url, true);
xhr.send();
}
export function apiPost(url, data, success, fail) {
let xhr = new XMLHttpRequest();
xhr.onload = function() {
// Process our return data
if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 300) {
// This will run when the request is successful
success(xhr);
} else {
// This will run when it's not
fail(xhr);
}
}
xhr.open("POST", url, true);
// console.log(data);
xhr.send(data);
}
b64_unicode.js
export function b64EncodeUnicode(str) {
// first we use encodeURIComponent to get percent-encoded UTF-8,
// then we convert the percent encodings into raw bytes which
// can be fed into btoa.
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode('0x' + p1);
}));
}
export function b64DecodeUnicode(str) {
// Going backwards: from bytestream, to percent-encoding, to original string.
return decodeURIComponent(atob(str).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}