// widget_jsontable.ysl2 widget_desc("JsonTable") { longdesc || 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("JsonTable") || // arbitrary defaults to avoid missing entries in query cache = [0,0,0]; init_common() { 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); this.promised = false; } handle_http_response(response) { if (!response.ok) { console.log("HTTP error, status = " + response.status); } return response.json(); } fetch_error(e){ 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(() => { const query = { args: this.args, range: this.cache[1], position: this.cache[2], visible: this.visible, extra: this.cache.slice(4), options: opt }; const options = { method: 'POST', 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); }); } unsub(){ this.abort_controller.abort(); super.unsub(); } sub(...args){ this.cache[0] = undefined; super.sub(...args); } dispatch(value, oldval, index) { if(this.cache[index] != value) this.cache[index] = value; else return; if(!this.promised){ this.promised = true; this.do_http_request().finally(() => { this.promised = false; }); } } make_on_click(...options){ let that = this; return function(evt){ that.do_http_request(...options); } } // on_click(evt, ...options) { // this.do_http_request(...options); // } || gen_index_xhtml { template "svg:*", mode="json_table_elt_render" { error > JsonTable Widget can't contain element of type «local-name()». } def "func:json_expressions" { param "expressions"; param "label"; // compute javascript expressions to access JSON data // described in given svg element's "label" // knowing that parent element already has given "expressions". choose { when "$label" { const "suffixes", "str:split($label)"; const "res" foreach "$suffixes" expression { const "suffix","."; const "pos","position()"; // take last available expression (i.e can have more suffixes than expressions) const "expr","$expressions[position() <= $pos][last()]/expression"; choose { when "contains($suffix,'=')" { const "name", "substring-before($suffix,'=')"; if "$expr/@name[. != $name]" error > JsonTable : misplaced '=' or inconsistent names in Json data expressions. attrib "name" value "$name"; attrib "content" > «$expr/@content»«substring-after($suffix,'=')» } otherwise { copy "$expr/@name"; attrib "content" > «$expr/@content»«$suffix» } } } result "exsl:node-set($res)"; } // Empty labels are ignored, expressions are then passed as-is. otherwise result "$expressions"; } } const "initexpr" expression attrib "content" > jdata const "initexpr_ns", "exsl:node-set($initexpr)"; template "svg:use", mode="json_table_elt_render" { param "expressions"; // 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]"; choose { 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»]; } otherwise warning > Clones (svg:use) in JsonTable 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" { param "expressions"; const "value_expr", "$expressions/expression[1]/@content"; const "original", "@original"; const "from_textstylelist", "$textstylelist_related_ns/list[elt/@eltid = $original]"; choose { 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 JsonTable Widget pointing to a HMI:TextStyleList widget or item must have a "textContent=.someVal" assignment following value expression in label. | { | let elt = id("«@id»"); | elt.textContent = String(«$content_expr»); | elt.style = hmi_widgets["«$from_textstylelist/@listid»"].styles[«$value_expr»]; | } } otherwise { | id("«@id»").textContent = String(«$value_expr»); } } } template "svg:image", mode="json_table_elt_render" { param "expressions"; const "value_expr", "$expressions/expression[1]/@content"; | id("«@id»").setAttribute('href', String(«$value_expr»)); } // only labels coming from Json widget are counted in def "func:filter_non_widget_label" { param "elt"; param "widget_elts"; const "eltid" choose { when "$elt/@original" value "$elt/@original"; otherwise value "$elt/@id"; } result "$widget_elts[@id=$eltid]/@inkscape:label"; } template "svg:*", mode="json_table_render_except_comments"{ param "expressions"; param "widget_elts"; 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"; with "label", "$label"; } } template "svg:*", mode="json_table_render" { param "expressions"; param "widget_elts"; param "label"; const "new_expressions", "func:json_expressions($expressions, $label)"; const "elt","."; 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" { param "expressions"; param "widget_elts"; param "label"; // use intermediate variables for optimization const "varprefix" > obj_«@id»_ | try { foreach "$expressions/expression"{ | let «$varprefix»«position()» = «@content»; | if(«$varprefix»«position()» == undefined) { | throw null; | } } // because we put values in a variables, we can replace corresponding expression with variable name const "new_expressions" foreach "$expressions/expression" xsl:copy { copy "@name"; 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:json_expressions(exsl:node-set($new_expressions), $label)"; with "widget_elts", "$widget_elts"; } | } catch(err) { | id("«@id»").style = "display:none"; | } } } widget_defs("JsonTable") { labels("data"); const "data_elt", "$result_svg_ns//*[@id = $hmi_element/@id]/*[@inkscape:label = 'data']"; | visible: «count($data_elt/*[@inkscape:label])», | spread_json_data: function(janswer) { | let [range,position,jdata] = janswer; | [[1, range], [2, position], [3, this.visible]].map(([i,v]) => { | this.apply_hmi_value(i,v); | this.cache[i] = v; | }); apply "$data_elt", mode="json_table_render_except_comments" { with "expressions","$initexpr_ns"; with "widget_elts","$hmi_element/*[@inkscape:label = 'data']/descendant::svg:*"; } | }, | init() { | this.init_common(); foreach "$hmi_element/*[starts-with(@inkscape:label,'action_')]" { | id("«@id»").onclick = this.make_on_click("«func:escape_quotes(@inkscape:label)»"); } | } }