Adventures in Custom Elements... making the html A tag

A_tag_like_element

So you click on the Plus sign, or maybe just on the element itself, and it fires off some javascript code that opens a “linked” diagram in a different tab.

That is the main idea. So it gives me a way to create objects that can be used to represent other diagrams which can be opened on their own. You can do that in the diagram opened up as well and link to yet more diagrams. Keep going this way inception style as much as you like. You want to get back to where you were? Close the browser tab it was opened up in, or shift back the the tab you came from.

So you can make a process diagram that can lead to other process diagrams. I would probably make an api that would open the diagram with my modeler with some code that references a saved bpmn xml in my microsoft sql database (already have that part done actually).

Does that make sense as to what the mock-up design is intended to be?

Hoping I am making this more clear instead of less. Thank you immensely for your time and help!

x Jeremy M.

If I understand you correctly, you request two features:

(1) Opening multiple diagrams in different tabs: This essentially means you got multiple modeler instances, that show different diagrams. Just build it and it would work (cf. Camunda Modeler, an application that implements this).

(2) Opening linked diagrams: Following the interaction example you may be able to register to ‘click’ events on an element. Simply intercept that click, figure out which diagram is linked (this is the tricky part) and proceed to step (1).

As far as I am aware non of your requirements requires an actual HTML<a/> tag to be rendered / injected.

Yes. I think you got it right. I didn’t want a literal A tag to be rendered. I wanted to make something that worked much like an A tag. Gives me the same ability to click on an element, and it will open a different document or different full instance of the modeler in a different browser tab or browser window. I would click on it and it would work much like (but not exactly as):

<a href="https://MyReallyCoolModelerInstances.com/bpmn_modeler/?database_id=3bsdj354" target="_blank"></a>

Again I don’t need it to create a literal A tag to be clicked on. I just need to make it so when the element is clicked on it will have the data somehow stored on the XML that has “3bsdj354”, and it will know to fire off the javascript code:

let url = "https://MyReallyCoolModelerInstances.com/bpmn_modeler/?database_id=" + database_id;
window.open(url, "_blank");

I am pretty sure I can accomplish this with the existing features and code already in BPMN. The hard part as you said is making it so the database_id is saved in and loaded from the bpmn xml. Then I need to either create a new custom object that can do the actions, or I need to be able to add that data to an existing object and make it so if that existing object has said extra data then it responds a bit different when clicked. Something like that.

Also trying very hard to make the saved bpmn xml be something that can be loaded without issue into other bpmn xml based modelers like Cawemo and the like. Why I would like to try to give maybe an existing element a bit of double duty if I can somehow add additional data to it in a way that the base modeler will validate properly.

We’re exporting valid XML and Cawemo uses our toolkit under the hood :drum:. Everyting you can feed into bpmn-js as a library you’ll also be able to feed into Cawemo.

Very true. Just afraid that if I make a custom object/element that it will stop working on cawemo so I want to do something that will mesh well without losing the data even if it may not quite work right on cawemo. If that makes sense at all.

I have been looking at these items:

which also references:

and I am wondering if this is closer to the mark I seem to be wanting to hit? Am I barking up the right tree you think?

Regardless of the way you implement your hyperlinks it’s not possible to implement them in way that a URL is opened whenever you click on an element. That’s not possible without customizing the modeler itself.

I have seen where you can detect when an element is clicked on you can do things. If I can detect that the element has additional data on it (I am assuming that event bus broadcast will have the element info attached to it), then I can check to see if a particular set of data is there, and then run a function tied to that event which runs some javascript. I grab the Database_ID from the data on the element, and run the javascript that does a window.open(“my url”+Database_ID, “_blank”); and presto a new window is opened with the script on that url know what to do with the code and opening a new modeler with a different bpmn.xml in it.

So of that I know how I am going to do it. When I actually get something put together that can do it, then I will be happy to share that code on here as well. What I don’t know is how I am going to get that data into the model in such a way that it will save and load in the bpmn.xml that is generated when calling a modeler.saveXML(). I also have the constraint that the way it saves the data should be something that preferably will not prevent it from being loaded “as is” into cawemo. Even if used in cawemo it shouldn’t lose that data either. Should go in and come back out of cawemo unaffected, and my modeler would know what to do with it when present.

So what I am mostly trying to figure out is how to get the data into the model in a compliant way, and have it save and load without issue. How I use the data will be a special thing I do by branching off a element click event and pulling desired data and doing something special.

A lot hinges on what I can and cannot put into the xml save in a way that it will be loaded later without issue.

You can use custom click handlers to open URLs on click. That approach will not help users of Cawemo, though, as you can’t customize the Cawemo modeler.

That is fine so long as the save file of bpmn xml still has the data that can be read by my site. It is ok that it is not used in cawemo.

Do you want these hyperlink elements to be seperate elements? You could also just have a custom attribute on an element and offer opening the specified link.

Not separate elements. Just custom attributes that I will set my modeler to react to when the element is clicked on. Yet something that properly saves into the XML, and doesn’t create a fuss in cawemo the the xml is passed into that app, and will pass out of cawemo to if exported as a bpmn xml file.

Basically overload an existing object (element?) with additional data, and I will have a click event that uses that data to open a new browser window or tab with the info in the tagged into the URL so the modeler/viewer knows with database data to load and use for the xml of that new tab or window.

That is easily possible. Have you had a look at this example?

Ironically… I had mention that URL and link in a above post of my own where I thought it might be an example that will lead me in the right direction. With your suggestion on top of my hunch, I think this may be more of the way and direction I will go. I will need to do some research and testing to see how it turns out. I will try to pop back on the thread to drop any useful results of my work so it can hopefully help out others who face similar challenges.

Thank you for your help with this. If I have any other questions about this I may also pop back into this thread to address them.

x Jeremy M.

OK wall of code time:

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 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 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();
}

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];
  }

  // 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);
  };
}

In that wall of code I have defined two moddle extensions:

{
  "name": "QualityAssurance",
  "uri": "http://some-company/schema/bpmn/qa",
  "prefix": "qa",
  "xml": {
    "tagAlias": "lowerCase"
  },
  "types": [
    {
      "name": "AnalyzedNode",
      "extends": [
        "bpmn:FlowNode"
      ],
      "properties": [
        {
          "name": "suitable",
          "isAttr": true,
          "type": "Float"
        }
      ]
    },
    {
      "name": "AnalysisDetails",
      "superClass": [ "Element" ],
      "properties": [
        {
          "name": "lastChecked",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "nextCheck",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "comments",
          "isMany": true,
          "type": "Comment"
        }
      ]
    },
    {
      "name": "Comment",
      "properties": [
        {
          "name": "author",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "text",
          "isBody": true,
          "type": "String"
        }
      ]
    }
  ],
  "emumerations": [],
  "associations": []
}
{
  "name": "UrlReference",
  "uri": "http://url_reference",
  "prefix": "url",
  "xml": {
    "tagAlias": "lowerCase"
  },
  "types": [
    {
      "name": "ref",
      "superClass": [ "Element" ],
      "properties": [
        {
          "name": "location",
          "isAttr": true,
          "type": "String"
        }
      ]
    }
  ],
  "emumerations": [],
  "associations": []
}

QA is just from the example to see that it works, and it does. Saves out, and loads in with no issue.

The one I made called URL REF seems to work, and it shows up in the BPMN XML that it saves out when I click to save it. Yet when I try to load it I get this error:

read.js:677 could not parse node
handleError @ read.js:677
handleOpen @ read.js:778
(anonymous) @ read.js:828
parse @ index.esm.js:1005
Parser.parse @ index.esm.js:297
(anonymous) @ read.js:847
setTimeout (async)
defer @ read.js:81
./node_modules/moddle-xml/lib/read.js.Reader.fromXML @ read.js:843
./node_modules/bpmn-moddle/lib/bpmn-moddle.js.BpmnModdle.fromXML @ bpmn-moddle.js:56
./node_modules/bpmn-js/lib/Viewer.js.Viewer.importXML @ Viewer.js:168
openDiagram @ app.js:182
reader.onload @ app.js:320
load (async)
handleFileSelect @ app.js:318
read.js:678 Error: unknown type <url:Ref>
    at Registry../node_modules/moddle/lib/registry.js.Registry.mapTypes (registry.js:159)
    at Registry../node_modules/moddle/lib/registry.js.Registry.getEffectiveDescriptor (registry.js:184)
    at BpmnModdle../node_modules/moddle/lib/moddle.js.Moddle.getType (moddle.js:97)
    at ElementHandler../node_modules/moddle-xml/lib/read.js.ElementHandler.getPropertyForNode (read.js:404)
    at ElementHandler../node_modules/moddle-xml/lib/read.js.ElementHandler.handleChild (read.js:458)
    at ElementHandler../node_modules/moddle-xml/lib/read.js.BaseElementHandler.handleNode (read.js:257)
    at handleOpen (read.js:775)
    at read.js:828
    at parse (index.esm.js:1005)
    at Parser.parse (index.esm.js:297)
handleError @ read.js:678
handleOpen @ read.js:778
(anonymous) @ read.js:828
parse @ index.esm.js:1005
Parser.parse @ index.esm.js:297
(anonymous) @ read.js:847
setTimeout (async)
defer @ read.js:81
./node_modules/moddle-xml/lib/read.js.Reader.fromXML @ read.js:843
./node_modules/bpmn-moddle/lib/bpmn-moddle.js.BpmnModdle.fromXML @ bpmn-moddle.js:56
./node_modules/bpmn-js/lib/Viewer.js.Viewer.importXML @ Viewer.js:168
openDiagram @ app.js:182
reader.onload @ app.js:320
load (async)
handleFileSelect @ app.js:318

I don’t really know why. I guess that I am defining improperly the URL REF definition. Yet that seems odd because it seems to create the xml items without issue, but it can’t seem to understand what they are on load.

This is the file I am trying to load in that produces the error:

<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:url="http://url_reference" xmlns:qa="http://some-company/schema/bpmn/qa" id="6d9687b2-4884-6387-8109-a0afe142b518" targetNamespace="http://bpmn.io/schema/bpmn" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd" diagram-name="test_2018-12-14-Dec">
  <bpmn2:process id="Process_1" isExecutable="false">
    <bpmn2:startEvent id="StartEvent_1">
      <bpmn2:extensionElements>
        <url:ref location="https://www.google.com/" />
      </bpmn2:extensionElements>
      <bpmn2:outgoing>SequenceFlow_1a2fxeb</bpmn2:outgoing>
    </bpmn2:startEvent>
    <bpmn2:task id="Task_0fa8dnk">
      <bpmn2:extensionElements>
        <qa:analysisDetails lastChecked="2015-01-20" nextCheck="2015-07-15">
          <qa:comment author="Klaus">Our operators always have a hard time to figure out, what they need to do here.</qa:comment>
          <qa:comment author="Walter">I believe this can be split up in a number of activities and partly automated.</qa:comment>
        </qa:analysisDetails>
      </bpmn2:extensionElements>
      <bpmn2:incoming>SequenceFlow_1a2fxeb</bpmn2:incoming>
      <bpmn2:outgoing>SequenceFlow_0adlmng</bpmn2:outgoing>
    </bpmn2:task>
    <bpmn2:endEvent id="EndEvent_0cl9wo8">
      <bpmn2:incoming>SequenceFlow_0adlmng</bpmn2:incoming>
    </bpmn2:endEvent>
    <bpmn2:sequenceFlow id="SequenceFlow_1a2fxeb" sourceRef="StartEvent_1" targetRef="Task_0fa8dnk" />
    <bpmn2:sequenceFlow id="SequenceFlow_0adlmng" sourceRef="Task_0fa8dnk" targetRef="EndEvent_0cl9wo8" />
  </bpmn2:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="412" y="240" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Task_0fa8dnk_di" bpmnElement="Task_0fa8dnk">
        <dc:Bounds x="505" y="218" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="EndEvent_0cl9wo8_di" bpmnElement="EndEvent_0cl9wo8">
        <dc:Bounds x="670" y="240" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="SequenceFlow_1a2fxeb_di" bpmnElement="SequenceFlow_1a2fxeb">
        <di:waypoint x="448" y="258" />
        <di:waypoint x="505" y="258" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="SequenceFlow_0adlmng_di" bpmnElement="SequenceFlow_0adlmng">
        <di:waypoint x="605" y="258" />
        <di:waypoint x="670" y="258" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn2:definitions>

As you can see… the QA parts are still in there, and the newly created URL REFs are in there. I don’t seem to get an error for the QA on the load, but the URL REFs do error on load. I feel like I am doing the same thing in both cases… and I don’t know why one works, and the other doesn’t.

If you need me to grab in more code and drop it, then I can.

Any help or suggestions on this?

x Jeremy M.

The problem is within the moddle extension. If you want your type name to be lowercased (like "name": "ref"), you should never use "tagAlias": "lowerCase". Instead of that, either remove the whole tagAlias or set it to upperCase.

Best,

Maciej

Thank you very much… I removed that part and now the moddle def which I have come to call:
urlRefPackage.json

Looks more like this:

{
  "name": "UrlReference",
  "uri": "http://url_reference",
  "prefix": "url",
  "xml": {
  },
  "types": [
    {
      "name": "ref",
      "superClass": [ "Element" ],
      "properties": [
        {
          "name": "location",
          "isAttr": true,
          "type": "String"
        }
      ]
    }
  ],
  "emumerations": [],
  "associations": []
}

It now seems to be loading and save fine with warnings or issues anymore, and the data stays in places after repeated loads and saves and no longer gets stripped out in the load.

If you see anything else odd, or have any suggestions on how to improve this definition, then please let me know. I will have to be honest in saying that I have no idea how that definition works or what it is really doing, but does indeed seem to work now.

Thank you!

x Jeremy M.

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(''));
}

Hi @jeremy.mone, I have read the whole thread but talking about your original requirement that was to

  1. add custom element
  2. that custom element should get saved in xml.

Were you able to achieve it?

P.S. I have followed both examples of BPMN.io i.e. ‘Custom Elements’ and ‘Nyan Cats’ and I understand that for custom elements we cannot save them xml. Nyan cat gets saved but it overrides any existing definition that is bpmn:ServiceTask.

Currently I am working on adding a custom element so that I can use it as a generic comment box on my diagram that itself is not associated with any element. And it should be saved in xml.