ForEach widget is used to span a small set of widget over a larger set of
Idea is somewhat similar to relative page, but it all happens inside the
ForEach widget, no page involved.
Together with relative Jump widgets it can be used to build a menu to reach
relative pages covering many identical HMI_NODES siblings.
ForEach widget takes a HMI_CLASS name as argument and a HMI_NODE path as
Direct sub-elements can be either groups of widget to be spanned, labeled
"ClassName:offset", or buttons to control the spanning, labeled
In case of "ClassName:offset", offset for first element is 1.
shortdesc > span widgets over a set of repeated HMI_NODEs
arg name="class_name" accepts="string" > HMI_CLASS name
path name="root" accepts="HMI_NODE" > where to find HMI_NODEs whose HMI_CLASS is class_name
path name="position" accepts="HMI_INT" > position of HMI_NODE mapped to first item, among similar siblings
path name="range" accepts="HMI_INT" count="optional" > count of HMI_NODE siblings
path name="size" accepts="HMI_INT" count="optional" > count of visible items
if "count(path) < 1" error > ForEach widget «$hmi_element/@id» must have one HMI path given.
if "count(arg) != 1" error > ForEach widget «$hmi_element/@id» must have one argument given : a class name.
const "class","arg[1]/@value";
const "base_path","path/@value";
const "hmi_index_base", "$indexed_hmitree/*[@hmipath = $base_path]";
const "hmi_tree_base", "$hmitree/descendant-or-self::*[@path = $hmi_index_base/@path]";
const "hmi_tree_items", "$hmi_tree_base/*[@class = $class]";
const "hmi_index_items", "$indexed_hmitree/*[@path = $hmi_tree_items/@path]";
const "items_paths", "$hmi_index_items/@hmipath";
foreach "$hmi_index_items" {
| «@index»`if "position()!=last()" > ,`
const "prefix","concat($class,':')";
const "buttons_regex","concat('^',$prefix,'[+\-][0-9]+')";
const "buttons", "$hmi_element/*[regexp:test(@inkscape:label, $buttons_regex)]";
const "op","substring-after(@inkscape:label, $prefix)";
| id("«@id»").setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_click('«$op»', evt)");
const "items_regex","concat('^',$prefix,'[0-9]+')";
const "unordered_items","$hmi_element//*[regexp:test(@inkscape:label, $items_regex)]";
foreach "$unordered_items" {
const "elt_label","concat($prefix, string(position()))";
const "elt","$unordered_items[@inkscape:label = $elt_label]";
const "pos","position()";
const "item_path", "$items_paths[$pos]";
| [ /* item="«$elt_label»" path="«$item_path»" */
if "count($elt)=0" error > Missing item labeled «$elt_label» in ForEach widget «$hmi_element/@id»
if "count($elt)>1" error > Duplicate item labeled «$elt_label» in ForEach widget «$hmi_element/@id»
foreach "func:refered_elements($elt)[@id = $hmi_elements/@id][not(@id = $elt/@id)]" {
if "not(func:is_descendant_path(func:widget(@id)/path/@value, $item_path))"
error > Widget id="«@id»" label="«@inkscape:label»" is having wrong path. Accroding to ForEach widget ancestor id="«$hmi_element/@id»", path should be descendant of "«$item_path»".
| hmi_widgets["«@id»"]`if "position()!=last()" > ,`
| ]`if "position()!=last()" > ,`
| range: «count($hmi_index_items)»,
| size: «count($unordered_items)»,
items_subscribed = false;
if(this.items_subscribed){
for(let item of this.items){
for(let widget of item) {
this.items_subscribed = false;
if(!this.items_subscribed){
for(let i = 0; i < this.size; i++) {
let item = this.items[i];
let orig_item_index = this.index_pool[i];
let item_index = this.index_pool[i+this.position];
let item_index_offset = item_index - orig_item_index;
item_index_offset += this.offset;
for(let widget of item) {
/* all variables of all widgets in a ForEach are all relative.
TODO: allow absolute variables in ForEach widgets
widget.sub(item_index_offset, widget.indexes.map(_=>true));
sub(new_offset, relativeness, container_id){
let position_given = this.indexes.length > 1;
// sub() will call apply_cache() and then dispatch()
// undefining position forces dispatch() to call apply_position()
this.position = undefined;
super.sub(new_offset, relativeness, container_id);
// if position isn't given as a variable
// dispatch() to call apply_position() aren't called
// and items must be subscibed now.
// as soon as subribed apply range and size once for all
if(this.indexes.length > 2)
this.apply_hmi_value(2, this.range);
if(this.indexes.length > 3)
this.apply_hmi_value(3, this.size);
apply_position(new_position){
let old_position = this.position;
let limited_position = Math.round(Math.max(Math.min(new_position, this.range - this.size), 0));
if(this.position == limited_position){
this.position = limited_position;
request_subscriptions_update();
jumps_need_update = true;
let new_position = eval(String(this.position)+opstr);
if(new_position + this.size > this.range) {
if(this.position + this.size == this.range)
new_position = this.range - this.size;
} else if(new_position < 0) {
new_position = this.range - this.size;
if(this.apply_position(new_position)){
this.apply_hmi_value(1, this.position);
dispatch(value, oldval, index) {
// Only care about position, others are constants
this.apply_position(value);
if(this.position != value){
// widget refused or apply different value, force it back
this.apply_hmi_value(1, this.position);