edouard@2994: // widget_jsontable.ysl2
edouard@2994: 
edouard@2994: template "widget[@type='JsonTable']", mode="widget_class"
edouard@2994:     ||
edouard@2994:     class JsonTableWidget extends Widget{
edouard@3069:         // arbitrary defaults to avoid missing entries in query
Edouard@3187:         cache = [0,0,0];
edouard@3181:         init_common() {
Edouard@3080:             this.spread_json_data_bound = this.spread_json_data.bind(this);
edouard@3149:             this.handle_http_response_bound = this.handle_http_response.bind(this);
edouard@3149:             this.fetch_error_bound = this.fetch_error.bind(this);
edouard@3149:             this.promised = false;
Edouard@3080:         }
Edouard@3080: 
Edouard@3080:         handle_http_response(response) {
Edouard@3080:             if (!response.ok) {
Edouard@3080:               console.log("HTTP error, status = " + response.status);
Edouard@3080:             }
Edouard@3080:             return response.json();
Edouard@3080:         }
Edouard@3080: 
edouard@3149:         fetch_error(e){
edouard@3149:             console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id);
edouard@3149:         }
edouard@3149: 
Edouard@3048:         do_http_request(...opt) {
edouard@3149:             this.abort_controller = new AbortController();
Edouard@3195:             return Promise.resolve().then(() => {
Edouard@3195: 
Edouard@3195:                 const query = {
Edouard@3195:                     args: this.args,
Edouard@3195:                     range: this.cache[1],
Edouard@3195:                     position: this.cache[2],
Edouard@3195:                     visible: this.visible,
Edouard@3195:                     extra: this.cache.slice(4),
Edouard@3195:                     options: opt
Edouard@3195:                 };
Edouard@3195: 
Edouard@3195:                 const options = {
Edouard@3195:                      method: 'POST',
Edouard@3195:                      body: JSON.stringify(query),
Edouard@3195:                      headers: {'Content-Type': 'application/json'},
Edouard@3195:                      signal: this.abort_controller.signal
Edouard@3195:                 };
Edouard@3195: 
Edouard@3195:                 return fetch(this.args[0], options)
Edouard@3195:                         .then(this.handle_http_response_bound)
Edouard@3195:                         .then(this.spread_json_data_bound)
Edouard@3195:                         .catch(this.fetch_error_bound);
Edouard@3195:             });
Edouard@3195:         }
Edouard@3195: 
edouard@3149:         unsub(){
edouard@3149:             this.abort_controller.abort();
edouard@3149:             super.unsub();
edouard@3149:         }
edouard@3149: 
Edouard@3189:         sub(...args){
Edouard@3189:             this.cache[0] = undefined;
Edouard@3189:             super.sub(...args);
Edouard@3189:         }
Edouard@3189: 
Edouard@3034:         dispatch(value, oldval, index) {
edouard@3149: 
edouard@3149:             if(this.cache[index] != value)
edouard@3149:                 this.cache[index] = value;
edouard@3149:             else
edouard@3149:                 return;
edouard@3149: 
edouard@3149:             if(!this.promised){
edouard@3149:                 this.promised = true;
Edouard@3150:                 this.do_http_request().finally(() => {
Edouard@3150:                     this.promised = false;
Edouard@3150:                 });
edouard@3149:             }
edouard@2996:         }
Edouard@3080:         make_on_click(...options){
Edouard@3081:             let that = this;
Edouard@3080:             return function(evt){
Edouard@3081:                 that.do_http_request(...options);
Edouard@3080:             }
Edouard@3080:         }
Edouard@3080:         // on_click(evt, ...options) {
Edouard@3080:         //     this.do_http_request(...options);
Edouard@3080:         // }
edouard@2994:     }
edouard@2994:     ||
edouard@2996: 
edouard@2996: template "svg:*", mode="json_table_elt_render" {
edouard@2997:     error > JsonTable Widget can't contain element of type «local-name()».
edouard@2996: }
edouard@2996: 
edouard@3028: 
edouard@3028: const "hmi_textstylelists_descs", "$parsed_widgets/widget[@type = 'TextStyleList']";
edouard@3028: const "hmi_textstylelists", "$hmi_elements[@id = $hmi_textstylelists_descs/@id]";
edouard@3028: 
Edouard@3031: const "textstylelist_related" foreach "$hmi_textstylelists" list {
Edouard@3031:     attrib "listid" value "@id";
Edouard@3031:     foreach "func:refered_elements(.)" elt {
Edouard@3031:         attrib "eltid" value "@id";
Edouard@3031:     }
Edouard@3031: }
Edouard@3031: const "textstylelist_related_ns", "exsl:node-set($textstylelist_related)";
Edouard@3031: 
Edouard@3031: def "func:json_expressions" {
Edouard@3031:     param "expressions";
Edouard@3031:     param "label";
Edouard@3031: 
Edouard@3031:     // compute javascript expressions to access JSON data
Edouard@3031:     // desscribed in given svg element's "label"
Edouard@3031:     // knowing that parent element already has given "expressions".
Edouard@3031: 
Edouard@3031:     choose {
Edouard@3031:         when "$label" {
Edouard@3031:             const "suffixes", "str:split($label)";
Edouard@3031:             const "res" foreach "$suffixes" expression {
Edouard@3031:                 const "suffix",".";
Edouard@3031:                 const "pos","position()";
Edouard@3031:                 // take last available expression (i.e can have more suffixes than expressions)
Edouard@3031:                 const "expr","$expressions[position() <= $pos][last()]/expression";
Edouard@3031:                 choose {
Edouard@3031:                     when "contains($suffix,'=')" {
Edouard@3031:                         const "name", "substring-before($suffix,'=')";
Edouard@3043:                         if "$expr/@name[. != $name]"
Edouard@3031:                             error > JsonTable : missplaced '=' or inconsistent names in Json data expressions.
Edouard@3031:                         attrib "name" value "$name";
Edouard@3031:                         attrib "content" > «$expr/@content»«substring-after($suffix,'=')»
Edouard@3031:                     }
Edouard@3031:                     otherwise {
Edouard@3031:                         copy "$expr/@name";
Edouard@3031:                         attrib "content" > «$expr/@content»«$suffix»
Edouard@3031:                     }
Edouard@3031:                 }
Edouard@3031:             }
Edouard@3031:             result "exsl:node-set($res)";
Edouard@3031:         }
Edouard@3031:         // Empty labels are ignored, expressions are then passed as-is.
Edouard@3031:         otherwise result "$expressions";
Edouard@3031:     }
Edouard@3031: 
Edouard@3031: }
Edouard@3031: 
Edouard@3031: const "initexpr" expression attrib "content" > jdata
Edouard@3031: const "initexpr_ns", "exsl:node-set($initexpr)";
Edouard@3031: 
edouard@2996: template "svg:use", mode="json_table_elt_render" {
Edouard@3031:     param "expressions";
edouard@2997:     // cloned element must be part of a HMI:List
edouard@2997:     const "targetid", "substring-after(@xlink:href,'#')";
edouard@2997:     const "from_list", "$hmi_lists[(@id | */@id) = $targetid]";
edouard@2997: 
edouard@3028:     choose {
edouard@3028:         when "count($from_list) > 0" {
Edouard@3081:             |         id("«@id»").setAttribute("xlink:href",
edouard@3028:             // obtain new target id from HMI:List widget
Edouard@3031:             |             "#"+hmi_widgets["«$from_list/@id»"].items[«$expressions/expression[1]/@content»]);
edouard@3028:         }
edouard@3028:         otherwise
Edouard@3031:             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.
edouard@3028:     }
edouard@2996: }
edouard@2996: 
edouard@2996: template "svg:text", mode="json_table_elt_render" {
Edouard@3031:     param "expressions";
Edouard@3031:     const "value_expr", "$expressions/expression[1]/@content";
Edouard@3031:     const "original", "@original";
Edouard@3031:     const "from_textstylelist", "$textstylelist_related_ns/list[elt/@eltid = $original]";
Edouard@3031:     choose {
Edouard@3031: 
Edouard@3031:         when "count($from_textstylelist) > 0" {
Edouard@3031:             const "content_expr", "$expressions/expression[2]/@content";
Edouard@3031:             if "string-length($content_expr) = 0 or $expressions/expression[2]/@name != 'textContent'"
Edouard@3031:                 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.
Edouard@3031:             |         {
Edouard@3031:             |           let elt = id("«@id»");
Edouard@3031:             |           elt.textContent = String(«$content_expr»);
Edouard@3031:             |           elt.style = hmi_widgets["«$from_textstylelist/@listid»"].styles[«$value_expr»];
Edouard@3031:             |         }
Edouard@3031:         }
Edouard@3031:         otherwise {
Edouard@3031:             |         id("«@id»").textContent = String(«$value_expr»);
Edouard@3031:         }
Edouard@3031:     }
Edouard@3031: }
Edouard@3031: 
Edouard@3031: 
Edouard@3031: // only labels comming from Json widget are counted in
Edouard@3031: def "func:filter_non_widget_label" {
Edouard@3031:     param "elt";
Edouard@3031:     param "widget_elts";
Edouard@3031:     const "eltid" choose {
Edouard@3031:         when "$elt/@original" value "$elt/@original";
Edouard@3031:         otherwise value "$elt/@id";
Edouard@3031:     }
Edouard@3031:     result "$widget_elts[@id=$eltid]/@inkscape:label";
edouard@2996: }
edouard@2996: 
Edouard@3043: template "svg:*", mode="json_table_render_except_comments"{
Edouard@3043:     param "expressions";
Edouard@3043:     param "widget_elts";
Edouard@3043: 
Edouard@3043:     const "label", "func:filter_non_widget_label(., $widget_elts)";
Edouard@3043:     // filter out "# commented" elements
Edouard@3043:     if "not(starts-with($label,'#'))" 
Edouard@3043:         apply ".", mode="json_table_render"{
Edouard@3043:             with "expressions", "$expressions";
Edouard@3043:             with "widget_elts", "$widget_elts";
Edouard@3043:             with "label", "$label";
Edouard@3043:         }
Edouard@3043: }
Edouard@3043: 
Edouard@3048: 
edouard@2996: template "svg:*", mode="json_table_render" {
Edouard@3031:     param "expressions";
Edouard@3031:     param "widget_elts";
Edouard@3043:     param "label";
Edouard@3048: 
Edouard@3048:     const "new_expressions", "func:json_expressions($expressions, $label)";
Edouard@3048: 
Edouard@3048:     const "elt",".";
Edouard@3048:     foreach "$new_expressions/expression[position() > 1][starts-with(@name,'onClick')]"
Edouard@3081:     |         id("«$elt/@id»").onclick = this.make_on_click('«@name»', «@content»);
Edouard@3048: 
Edouard@3043:     apply ".", mode="json_table_elt_render"
Edouard@3048:         with "expressions", "$new_expressions";
edouard@2996: }
edouard@2996: 
edouard@2996: template "svg:g", mode="json_table_render" {
Edouard@3031:     param "expressions";
Edouard@3031:     param "widget_elts";
Edouard@3043:     param "label";
Edouard@3036: 
Edouard@3036:     // use intermediate variables for optimization
edouard@3149:     const "varprefix" > obj_«@id»_
Edouard@3036:     |         try {
Edouard@3036: 
Edouard@3036:     foreach "$expressions/expression"{
Edouard@3036:     |          let «$varprefix»«position()» = «@content»;
Edouard@3036:     |          if(«$varprefix»«position()» == undefined) {
Edouard@3036:     |               throw null;
Edouard@3036:     |          }
Edouard@3036:     }
Edouard@3036: 
Edouard@3036:     // because we put values in a variables, we can replace corresponding expression with variable name
Edouard@3036:     const "new_expressions" foreach "$expressions/expression" xsl:copy {
Edouard@3036:         copy "@name";
Edouard@3036:         attrib "content" > «$varprefix»«position()»
Edouard@3036:     }
Edouard@3036: 
Edouard@3036:     // revert hiding in case it did happen before
Edouard@3080:     |           id("«@id»").style = "«@style»";
Edouard@3036: 
Edouard@3043:     apply "*", mode="json_table_render_except_comments" {
Edouard@3036:         with "expressions", "func:json_expressions(exsl:node-set($new_expressions), $label)";
Edouard@3031:         with "widget_elts", "$widget_elts";
Edouard@3031:     }
Edouard@3036:     |         } catch(err) {
edouard@3149:     |           id("«@id»").style = "display:none";
Edouard@3036:     |         }
edouard@2996: }
edouard@2996: 
edouard@2996: template "widget[@type='JsonTable']", mode="widget_defs" {
edouard@2996:     param "hmi_element";
edouard@2996:     labels("data");
edouard@2996:     const "data_elt", "$result_svg_ns//*[@id = $hmi_element/@id]/*[@inkscape:label = 'data']";
Edouard@3036:     |     visible: «count($data_elt/*[@inkscape:label])»,
Edouard@3036:     |     spread_json_data: function(janswer) {
Edouard@3036:     |         let [range,position,jdata] = janswer;
edouard@3149:     |         [[1, range], [2, position], [3, this.visible]].map(([i,v]) => {
edouard@3149:     |              this.apply_hmi_value(i,v);
edouard@3149:     |              this.cache[i] = v;
edouard@3149:     |         });
Edouard@3043:     apply "$data_elt", mode="json_table_render_except_comments" {
Edouard@3031:         with "expressions","$initexpr_ns";
Edouard@3031:         with "widget_elts","$hmi_element/*[@inkscape:label = 'data']/descendant::svg:*";
Edouard@3031:     }
edouard@3181:     |     },
edouard@3181:     |     init() {
edouard@3181:     |        this.init_common();
edouard@3181:     foreach "$hmi_element/*[starts-with(@inkscape:label,'action_')]" {
edouard@3181:     |         id("«@id»").onclick = this.make_on_click("«func:escape_quotes(@inkscape:label)»");
edouard@3181:     }
edouard@2997:     |     }
edouard@3181: 
edouard@3181: }