SVGHMI: add automatic reconnection of websocket with 1s reconnection delay after first attempt.
// 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 exemple.
||
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
// desscribed 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 : missplaced '=' 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" assignement 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»);
}
}
}
// only labels comming 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)»");
}
| }
}