// widget_multilangjsontable.ysl2
widget_desc("MultiLangJsonTable") {
Send given variables as POST to http URL argument, spread returned JSON in
SVG sub-elements of "data" labeled element.
Documentation to be written. see svghmi example.
shortdesc > Http POST variables, spread JSON back
arg name="url" accepts="string" >
path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING" > single variable to edit
widget_class("MultiLangJsonTable")
// arbitrary defaults to avoid missing entries in query
this.spread_json_data_bound = this.spread_json_data.bind(this);
this.handle_http_response_bound = this.handle_http_response.bind(this);
this.fetch_error_bound = this.fetch_error.bind(this);
if (this.should_translate === undefined) {
this.should_translate = [];
if (this.lang_keys === undefined) {
handle_http_response(response) {
console.log("HTTP error, status = " + response.status);
console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id);
do_http_request(...opt) {
this.abort_controller = new AbortController();
return Promise.resolve().then(() => {
extra: this.cache.slice(4),
body: JSON.stringify(query),
headers: {'Content-Type': 'application/json'},
signal: this.abort_controller.signal
return fetch(this.args[0], options)
.then(this.handle_http_response_bound)
.then(this.spread_json_data_bound)
.catch(this.fetch_error_bound);
this.abort_controller.abort();
this.cache[0] = undefined;
dispatch(value, oldval, index) {
if(this.cache[index] != value)
this.cache[index] = value;
this.do_http_request().finally(() => {
make_on_click(...options){
that.do_http_request(...options);
// on_click(evt, ...options) {
// this.do_http_request(...options);
template "svg:*", mode="json_table_elt_render" {
error > MultiLangJsonTable Widget can't contain element of type «local-name()».
def "func:ml_json_expressions" {
const "suffixes", "str:split($label)";
const "res" foreach "$suffixes" expression {
const "pos", "position()";
const "expr", "$expressions[position() <= $pos][last()]/expression";
const "raw_selector", "$suffix";
const "lang_selector" choose {
when "starts-with($raw_selector, '.')" value "substring($raw_selector, 2)";
otherwise value "$raw_selector";
attrib "lang_selector" value "$lang_selector";
when "contains($suffix, '=')" {
const "name", "substring-before($suffix, '=')";
const "content_raw", "substring-after($suffix, '=')";
if "$expr/@name[. != $name]"
error > MultiLangJsonTable : misplaced '=' or inconsistent names in Json data expressions.
attrib "name" value "$name";
when "starts-with($content_raw, '_(') and substring($content_raw, string-length($content_raw)) = ')'" {
const "raw_key", "substring($content_raw, 3, string-length($content_raw) - 3)";
const "clean_key" choose {
when "starts-with($raw_key, '.')" value "substring($raw_key, 2)";
otherwise value "$raw_key";
attrib "translation_key" value "$clean_key";
attrib "content" > «$expr/@content»«$raw_key»
attrib "content" > «$expr/@content»«$content_raw»
attrib "content" > «$expr/@content»«$suffix»
result "exsl:node-set($res)";
otherwise result "$expressions";
const "ml_initexpr" expression attrib "content" > jdata
const "ml_initexpr_ns", "exsl:node-set($ml_initexpr)";
template "svg:use", mode="json_table_elt_render" {
// cloned element must be part of a HMI:List or a HMI:List
const "targetid", "substring-after(@xlink:href,'#')";
const "from_list", "$hmi_lists[(@id | */@id) = $targetid]";
when "count($from_list) > 0" {
| id("«@id»").href.baseVal =
// obtain new target id from HMI:List widget
| "#"+hmi_widgets["«$from_list/@id»"].items[«$expressions/expression[1]/@content»];
warning > Clones (svg:use) in MultiLangJsonTable Widget must point to a valid HMI:List widget or item. Reference "«@xlink:href»" is not valid and will not be updated.
template "svg:text", mode="json_table_elt_render" {
const "value_expr", "$expressions/expression[1]/@content";
const "original", "@original";
const "from_textstylelist", "$textstylelist_related_ns/list[elt/@eltid = $original]";
when "count($from_textstylelist) > 0" {
const "content_expr", "$expressions/expression[2]/@content";
if "string-length($content_expr) = 0 or $expressions/expression[2]/@name != 'textContent'"
error > Clones (svg:use) in MultiLangJsonTable Widget pointing to a HMI:TextStyleList widget or item must have a "textContent=.someVal" assignment following value expression in label.
| elt.textContent = String(«$content_expr»);
| elt.style = hmi_widgets["«$from_textstylelist/@listid»"].styles[«$value_expr»];
| id("«@id»").textContent = String(«$value_expr»);
template "svg:image", mode="json_table_elt_render" {
const "value_expr", "$expressions/expression[1]/@content";
| id("«@id»").setAttribute('href', String(«$value_expr»));
template "svg:*", mode="json_table_render_except_comments"{
const "label", "func:filter_non_widget_label(., $widget_elts)";
// filter out "# commented" elements
if "not(starts-with($label,'#'))"
apply ".", mode="json_table_render"{
with "expressions", "$expressions";
with "widget_elts", "$widget_elts";
template "svg:*", mode="json_table_render" {
const "new_expressions", "func:ml_json_expressions($expressions, $label)";
foreach "$new_expressions/expression[position() > 1][starts-with(@name,'onClick')]"
| id("«$elt/@id»").onclick = this.make_on_click('«@name»', «@content»);
apply ".", mode="json_table_elt_render"
with "expressions", "$new_expressions";
template "svg:g", mode="json_table_render" {
// use intermediate variables for optimization
const "varprefix" > obj_«@id»_
foreach "$expressions/expression"{
| let «$varprefix»«position()» = «@content»;
| if(«$varprefix»«position()» == undefined) {
// because we put values in a variables, we can replace corresponding expression with variable name
const "new_expressions" foreach "$expressions/expression" xsl:copy {
attrib "content" > «$varprefix»«position()»
// revert hiding in case it did happen before
| id("«@id»").style = "«@style»";
apply "*", mode="json_table_render_except_comments" {
with "expressions", "func:ml_json_expressions(exsl:node-set($new_expressions), $label)";
with "widget_elts", "$widget_elts";
| id("«@id»").style = "display:none";
widget_defs("MultiLangJsonTable") {
const "data_elt", "$result_svg_ns//*[@id = $hmi_element/@id]/*[@inkscape:label = 'data']";
const "widget_elts", "$hmi_element/*[@inkscape:label = 'data']/descendant::svg:*";
const "all_parsed_expressions" {
foreach "$data_elt/descendant-or-self::svg:*[@inkscape:label]" {
const "exprs", "func:ml_json_expressions($ml_initexpr_ns, @inkscape:label)";
copy "exsl:node-set($exprs)//expression[@translation_key or @lang_selector]";
const "all_exprs_ns", "exsl:node-set($all_parsed_expressions)";
| visible: «count($data_elt/*[@inkscape:label])»,
foreach "$all_exprs_ns/expression[@translation_key]" {
if "not(@translation_key = preceding-sibling::expression/@translation_key)" {
if "position() != last()" {
foreach "$all_exprs_ns/expression[@lang_selector]" {
if "not(@lang_selector = preceding-sibling::expression/@lang_selector)" {
if "position() != last()" {
| spread_json_data: function(janswer) {
| let [range,position,jdata] = janswer;
| if (jdata.length > 0 && this.should_translate.length > 0) {
| const lang = cache[lang_local_index];
| const langcode = langs[lang][1];
| for (let row of jdata) {
| for (const key of this.should_translate) {
| const match = translations.find(item => item[1][0] == orig);
| const tr = match ? match[1][lang] : orig;
| for (const key of this.lang_keys) {
| row[key] = String(row[key]) + "_" + langcode;
| [[1, range], [2, position], [3, this.visible]].map(([i,v]) => {
| this.apply_hmi_value(i,v);
apply "$data_elt", mode="json_table_render_except_comments" {
with "expressions","$ml_initexpr_ns";
with "widget_elts","$widget_elts";
foreach "$hmi_element/*[starts-with(@inkscape:label,'action_')]" {
| id("«@id»").onclick = this.make_on_click("«func:escape_quotes(@inkscape:label)»");
foreach "$hmi_element/*[starts-with(@inkscape:label,'dict')]" {
| id("«@id»").style.display = "none";