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

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.

I did not end up making an actual custom element. What I ended up doing is adding data to a existing elements that can be read and acted on. In this case I added my own custom xml to bpmn2:extensionElements. I still needed to create a definition for this addition so that the moddle code would know how to read and write the XML, but I didn’t need to go full bore and create a actual custom element. I created this code to define my addition:

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

Which I loaded into my main javascript (app.js in my case) with this code:

import urlRefPackage from './moddleExtensions/urlRefPackage.json';

and then I added it to my modeler by adding it to my extensions part of the config for it, as so:

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

Note the urlRef under moddleExtensions. Now for anything you want to read and write in XML you will need to define a moddleExtension to support it. Otherwise it will not load and save via the XML file generated. I am pretty sure this is the same for a whole new custom element as it is for my small addition to the existing XML.

So for what I have learned from the examples given. In Nyan cats, that is interesting, but more of what that example is doing is over-riding an existing element with a bit of different data into particular places. Most specifically the graphic it uses. It doesn’t change nor create new rules for that object, or anything of the like. That is the reason why it is so short compared to the custom elements example.

Now the official custom elements example actually is creating new elements to be used, but they do NOT include the moddle extension data and definitions that would be required for it to read and write into the XML. I have no idea why they made an example that shows how to create custom elements that would not read and write into the xml save file generated. I would assume that a majority of people would want said custom elements to save and load in the output xml file created. I found this particularly frustrating because I didn’t have a clear cut example of not just making a custom element, but one that would indeed be present in any output xml created containing it.

So in that regard the two examples are lacking. I wished they made an example that makes ONE and just one custom element, and has the maximum number of things that can possibly be defined for a custom element so it is an example that shows how everything could be filled in, and also contains a moddleExtension or whatever definition that is needed for it to be saved into the outgoing xml and can be loaded back in with files with the same xml present for those elements. I am pretty sure every time someone asks for information about how to make a custom element, that is what they really mean and need. Perhaps an update to the existing custom element example that instead of relying on json to store the custom elements it has the moddle code so it loads and saves in the xml. That is all that is really needed for that, I think.


So quite a long post short, no I didn’t actually make a custom element for my solution. I used an alternative that used existing elements and added data to work with a click event on an element to replicate the A tag effect I was looking for.

Most of the info was part of this:


I found more info about XML definitions in moddle here:

This would be pretty helpful perhaps in creating the json definition of new XML items to read and write into XML output files.


Also maybe this can be useful as well:

@nikku and/or @philippfromme and/or other experienced members of the community … am I explaining or understanding this correctly. I think I have the right idea, but as I re-read this I feel a bit unsure about what I am saying. If I am incorrect, then could you clarify the information?

Thank you again for your time and help.

x Jeremy M.

So basically, you needed this example - as I already said.

If i have one of this kind of cutomized object, how can i dinamically insert one inside the xml? (using the js code)

Please do not necrobump old topics. Instead link to this thread from new topic.