svghmi/widget_dropdown.ysl2
branchsvghmi
changeset 3090 9e172e4e50c7
parent 3035 d1fc8c55c1d3
child 3091 f475f39713aa
equal deleted inserted replaced
3089:10b2e620b57f 3090:9e172e4e50c7
     1 // widget_dropdown.ysl2
     1 // widget_dropdown.ysl2
     2 
     2 
       
     3 template "widget[@type='DropDown']", mode="widget_class"{
       
     4 ||
       
     5     class DropDownWidget extends Widget{
       
     6         dispatch(value) {
       
     7             if(!this.opened) this.set_selection(value);
       
     8         }
       
     9         init() {
       
    10             this.button_elt.onclick = this.on_button_click.bind(this);
       
    11             // Save original size of rectangle
       
    12             this.box_bbox = this.box_elt.getBBox()
       
    13 
       
    14             // Compute margins
       
    15             let text_bbox = this.text_elt.getBBox();
       
    16             let lmargin = text_bbox.x - this.box_bbox.x;
       
    17             let tmargin = text_bbox.y - this.box_bbox.y;
       
    18             this.margins = [lmargin, tmargin].map(x => Math.max(x,0));
       
    19 
       
    20             // Index of first visible element in the menu, when opened
       
    21             this.menu_offset = 0;
       
    22 
       
    23             // How mutch to lift the menu vertically so that it does not cross bottom border
       
    24             this.lift = 0;
       
    25 
       
    26             // Event handlers cannot be object method ('this' is unknown)
       
    27             // as a workaround, handler given to addEventListener is bound in advance.
       
    28             this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this);
       
    29             this.bound_on_selection_click = this.on_selection_click.bind(this);
       
    30             this.bound_on_backward_click = this.on_backward_click.bind(this);
       
    31             this.bound_on_forward_click = this.on_forward_click.bind(this);
       
    32             this.opened = false;
       
    33         }
       
    34         on_button_click() {
       
    35             this.open();
       
    36         }
       
    37         // Called when a menu entry is clicked
       
    38         on_selection_click(selection) {
       
    39             this.close();
       
    40             this.apply_hmi_value(0, selection);
       
    41         }
       
    42         on_backward_click(){
       
    43             this.scroll(false);
       
    44         }
       
    45         on_forward_click(){
       
    46             this.scroll(true);
       
    47         }
       
    48         set_selection(value) {
       
    49             let display_str;
       
    50             if(value >= 0 && value < this.content.length){
       
    51                 // if valid selection resolve content
       
    52                 display_str = this.content[value];
       
    53                 this.last_selection = value;
       
    54             } else {
       
    55                 // otherwise show problem
       
    56                 display_str = "?"+String(value)+"?";
       
    57             }
       
    58             // It is assumed that first span always stays,
       
    59             // and contains selection when menu is closed
       
    60             this.text_elt.firstElementChild.textContent = display_str;
       
    61         }
       
    62         grow_text(up_to) {
       
    63             let count = 1;
       
    64             let txt = this.text_elt; 
       
    65             let first = txt.firstElementChild;
       
    66             // Real world (pixels) boundaries of current page
       
    67             let bounds = svg_root.getBoundingClientRect(); 
       
    68             this.lift = 0;
       
    69             while(count < up_to) {
       
    70                 let next = first.cloneNode();
       
    71                 // relative line by line text flow instead of absolute y coordinate
       
    72                 next.removeAttribute("y");
       
    73                 next.setAttribute("dy", "1.1em");
       
    74                 // default content to allow computing text element bbox
       
    75                 next.textContent = "...";
       
    76                 // append new span to text element
       
    77                 txt.appendChild(next);
       
    78                 // now check if text extended by one row fits to page
       
    79                 // FIXME : exclude margins to be more accurate on box size
       
    80                 let rect = txt.getBoundingClientRect();
       
    81                 if(rect.bottom > bounds.bottom){
       
    82                     // in case of overflow at the bottom, lift up one row
       
    83                     let backup = first.getAttribute("dy");
       
    84                     // apply lift asr a dy added too first span (y attrib stays)
       
    85                     first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em");
       
    86                     rect = txt.getBoundingClientRect();
       
    87                     if(rect.top > bounds.top){
       
    88                         this.lift += 1;
       
    89                     } else {
       
    90                         // if it goes over the top, then backtrack
       
    91                         // restore dy attribute on first span
       
    92                         if(backup)
       
    93                             first.setAttribute("dy", backup);
       
    94                         else
       
    95                             first.removeAttribute("dy");
       
    96                         // remove unwanted child
       
    97                         txt.removeChild(next);
       
    98                         return count;
       
    99                     }
       
   100                 }
       
   101                 count++;
       
   102             }
       
   103             return count;
       
   104         }
       
   105         close_on_click_elsewhere(e) {
       
   106             // inhibit events not targetting spans (menu items)
       
   107             if(e.target.parentNode !== this.text_elt){
       
   108                 e.stopPropagation();
       
   109                 // close menu in case click is outside box
       
   110                 if(e.target !== this.box_elt)
       
   111                     this.close();
       
   112             }
       
   113         }
       
   114         close(){
       
   115             // Stop hogging all click events
       
   116             svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true);
       
   117             // Restore position and sixe of widget elements
       
   118             this.reset_text();
       
   119             this.reset_box();
       
   120             // Put the button back in place
       
   121             this.element.appendChild(this.button_elt);
       
   122             // Mark as closed (to allow dispatch)
       
   123             this.opened = false;
       
   124             // Dispatch last cached value
       
   125             this.apply_cache();
       
   126         }
       
   127         // Set text content when content is smaller than menu (no scrolling)
       
   128         set_complete_text(){
       
   129             let spans = this.text_elt.children; 
       
   130             let c = 0;
       
   131             for(let item of this.content){
       
   132                 let span=spans[c];
       
   133                 span.textContent = item;
       
   134                 let sel = c;
       
   135                 span.onclick = (evt) => this.bound_on_selection_click(sel);
       
   136                 c++;
       
   137             }
       
   138         }
       
   139         // Move partial view :
       
   140         // false : upward, lower value
       
   141         // true  : downward, higher value
       
   142         scroll(forward){
       
   143             let contentlength = this.content.length;
       
   144             let spans = this.text_elt.children; 
       
   145             let spanslength = spans.length;
       
   146             // reduce accounted menu size according to jumps
       
   147             if(this.menu_offset != 0) spanslength--;
       
   148             if(this.menu_offset < contentlength - 1) spanslength--;
       
   149             if(forward){
       
   150                 this.menu_offset = Math.min(
       
   151                     contentlength - spans.length + 1, 
       
   152                     this.menu_offset + spanslength);
       
   153             }else{
       
   154                 this.menu_offset = Math.max(
       
   155                     0, 
       
   156                     this.menu_offset - spanslength);
       
   157             }
       
   158             this.set_partial_text();
       
   159         }
       
   160         // Setup partial view text content
       
   161         // with jumps at first and last entry when appropriate
       
   162         set_partial_text(){
       
   163             let spans = this.text_elt.children; 
       
   164             let contentlength = this.content.length;
       
   165             let spanslength = spans.length;
       
   166             let i = this.menu_offset, c = 0;
       
   167             while(c < spanslength){
       
   168                 let span=spans[c];
       
   169                 // backward jump only present if not exactly at start
       
   170                 if(c == 0 && i != 0){
       
   171                     span.textContent = "↑  ↑  ↑";
       
   172                     span.onclick = this.bound_on_backward_click;
       
   173                 // presence of forward jump when not right at the end
       
   174                 }else if(c == spanslength-1 && i < contentlength - 1){
       
   175                     span.textContent = "↓  ↓  ↓";
       
   176                     span.onclick = this.bound_on_forward_click;
       
   177                 // otherwise normal content
       
   178                 }else{
       
   179                     span.textContent = this.content[i];
       
   180                     let sel = i;
       
   181                     span.onclick = (evt) => this.bound_on_selection_click(sel);
       
   182                     i++;
       
   183                 }
       
   184                 c++;
       
   185             }
       
   186         }
       
   187         open(){
       
   188             let length = this.content.length;
       
   189             // systematically reset text, to strip eventual whitespace spans
       
   190             this.reset_text();
       
   191             // grow as much as needed or possible
       
   192             let slots = this.grow_text(length);
       
   193             // Depending on final size
       
   194             if(slots == length) {
       
   195                 // show all at once
       
   196                 this.set_complete_text();
       
   197             } else {
       
   198                 // eventualy align menu to current selection, compensating for lift
       
   199                 let offset = this.last_selection - this.lift;
       
   200                 if(offset > 0)
       
   201                     this.menu_offset = Math.min(offset + 1, length - slots + 1);
       
   202                 else
       
   203                     this.menu_offset = 0;
       
   204                 // show surrounding values
       
   205                 this.set_partial_text();
       
   206             }
       
   207             // Now that text size is known, we can set the box around it
       
   208             this.adjust_box_to_text();
       
   209             // Take button out until menu closed
       
   210             this.element.removeChild(this.button_elt);
       
   211             // Rise widget to top by moving it to last position among siblings
       
   212             this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element));
       
   213             // disable interaction with background
       
   214             svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true);
       
   215             // mark as open
       
   216             this.opened = true;
       
   217         }
       
   218         // Put text element in normalized state
       
   219         reset_text(){
       
   220             let txt = this.text_elt; 
       
   221             let first = txt.firstElementChild;
       
   222             // remove attribute eventually added to first text line while opening
       
   223             first.removeAttribute("onclick");
       
   224             first.removeAttribute("dy");
       
   225             // keep only the first line of text
       
   226             for(let span of Array.from(txt.children).slice(1)){
       
   227                 txt.removeChild(span)
       
   228             }
       
   229         }
       
   230         // Put rectangle element in saved original state
       
   231         reset_box(){
       
   232             let m = this.box_bbox;
       
   233             let b = this.box_elt;
       
   234             b.x.baseVal.value = m.x;
       
   235             b.y.baseVal.value = m.y;
       
   236             b.width.baseVal.value = m.width;
       
   237             b.height.baseVal.value = m.height;
       
   238         }
       
   239         // Use margin and text size to compute box size
       
   240         adjust_box_to_text(){
       
   241             let [lmargin, tmargin] = this.margins;
       
   242             let m = this.text_elt.getBBox();
       
   243             let b = this.box_elt;
       
   244             b.x.baseVal.value = m.x - lmargin;
       
   245             b.y.baseVal.value = m.y - tmargin;
       
   246             b.width.baseVal.value = 2 * lmargin + m.width;
       
   247             b.height.baseVal.value = 2 * tmargin + m.height;
       
   248         }
       
   249     }
       
   250 ||
       
   251 }
     3 template "widget[@type='DropDown']", mode="widget_defs" {
   252 template "widget[@type='DropDown']", mode="widget_defs" {
     4     param "hmi_element";
   253     param "hmi_element";
     5     labels("text box button");
   254     labels("text box button");
     6 ||
   255 ||
     7     dispatch: function(value) {
   256     // It is assumed that list content conforms to Array interface.
     8         if(!this.opened) this.set_selection(value);
   257     content: [
     9     },
   258     ``foreach "arg" | "«@value»",
    10     init: function() {
   259     ],
    11         this.button_elt.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_button_click()");
   260 
    12         // Save original size of rectangle
       
    13         this.box_bbox = this.box_elt.getBBox()
       
    14 
       
    15         // Compute margins
       
    16         text_bbox = this.text_elt.getBBox()
       
    17         lmargin = text_bbox.x - this.box_bbox.x;
       
    18         tmargin = text_bbox.y - this.box_bbox.y;
       
    19         this.margins = [lmargin, tmargin].map(x => Math.max(x,0));
       
    20 
       
    21         // It is assumed that list content conforms to Array interface.
       
    22         this.content = [
       
    23         ``foreach "arg" | "«@value»",
       
    24         ];
       
    25 
       
    26         // Index of first visible element in the menu, when opened
       
    27         this.menu_offset = 0;
       
    28 
       
    29         // How mutch to lift the menu vertically so that it does not cross bottom border
       
    30         this.lift = 0;
       
    31 
       
    32         // Event handlers cannot be object method ('this' is unknown)
       
    33         // as a workaround, handler given to addEventListener is bound in advance.
       
    34         this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this);
       
    35 
       
    36         this.opened = false;
       
    37     },
       
    38     // Called when a menu entry is clicked
       
    39     on_selection_click: function(selection) {
       
    40         this.close();
       
    41         this.apply_hmi_value(0, selection);
       
    42     },
       
    43     on_button_click: function() {
       
    44         this.open();
       
    45     },
       
    46     on_backward_click: function(){
       
    47         this.scroll(false);
       
    48     },
       
    49     on_forward_click:function(){
       
    50         this.scroll(true);
       
    51     },
       
    52     set_selection: function(value) {
       
    53         let display_str;
       
    54         if(value >= 0 && value < this.content.length){
       
    55             // if valid selection resolve content
       
    56             display_str = this.content[value];
       
    57             this.last_selection = value;
       
    58         } else {
       
    59             // otherwise show problem
       
    60             display_str = "?"+String(value)+"?";
       
    61         }
       
    62         // It is assumed that first span always stays,
       
    63         // and contains selection when menu is closed
       
    64         this.text_elt.firstElementChild.textContent = display_str;
       
    65     },
       
    66     grow_text: function(up_to) {
       
    67         let count = 1;
       
    68         let txt = this.text_elt; 
       
    69         let first = txt.firstElementChild;
       
    70         // Real world (pixels) boundaries of current page
       
    71         let bounds = svg_root.getBoundingClientRect(); 
       
    72         this.lift = 0;
       
    73         while(count < up_to) {
       
    74             let next = first.cloneNode();
       
    75             // relative line by line text flow instead of absolute y coordinate
       
    76             next.removeAttribute("y");
       
    77             next.setAttribute("dy", "1.1em");
       
    78             // default content to allow computing text element bbox
       
    79             next.textContent = "...";
       
    80             // append new span to text element
       
    81             txt.appendChild(next);
       
    82             // now check if text extended by one row fits to page
       
    83             // FIXME : exclude margins to be more accurate on box size
       
    84             let rect = txt.getBoundingClientRect();
       
    85             if(rect.bottom > bounds.bottom){
       
    86                 // in case of overflow at the bottom, lift up one row
       
    87                 let backup = first.getAttribute("dy");
       
    88                 // apply lift asr a dy added too first span (y attrib stays)
       
    89                 first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em");
       
    90                 rect = txt.getBoundingClientRect();
       
    91                 if(rect.top > bounds.top){
       
    92                     this.lift += 1;
       
    93                 } else {
       
    94                     // if it goes over the top, then backtrack
       
    95                     // restore dy attribute on first span
       
    96                     if(backup)
       
    97                         first.setAttribute("dy", backup);
       
    98                     else
       
    99                         first.removeAttribute("dy");
       
   100                     // remove unwanted child
       
   101                     txt.removeChild(next);
       
   102                     return count;
       
   103                 }
       
   104             }
       
   105             count++;
       
   106         }
       
   107         return count;
       
   108     },
       
   109     close_on_click_elsewhere: function(e) {
       
   110         // inhibit events not targetting spans (menu items)
       
   111         if(e.target.parentNode !== this.text_elt){
       
   112             e.stopPropagation();
       
   113             // close menu in case click is outside box
       
   114             if(e.target !== this.box_elt)
       
   115                 this.close();
       
   116         }
       
   117     },
       
   118     close: function(){
       
   119         // Stop hogging all click events
       
   120         svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true);
       
   121         // Restore position and sixe of widget elements
       
   122         this.reset_text();
       
   123         this.reset_box();
       
   124         // Put the button back in place
       
   125         this.element.appendChild(this.button_elt);
       
   126         // Mark as closed (to allow dispatch)
       
   127         this.opened = false;
       
   128         // Dispatch last cached value
       
   129         this.apply_cache();
       
   130     },
       
   131     // Set text content when content is smaller than menu (no scrolling)
       
   132     set_complete_text: function(){
       
   133         let spans = this.text_elt.children; 
       
   134         let c = 0;
       
   135         for(let item of this.content){
       
   136             let span=spans[c];
       
   137             span.textContent = item;
       
   138             span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_selection_click("+c+")");
       
   139             c++;
       
   140         }
       
   141     },
       
   142     // Move partial view :
       
   143     // false : upward, lower value
       
   144     // true  : downward, higher value
       
   145     scroll: function(forward){
       
   146         let contentlength = this.content.length;
       
   147         let spans = this.text_elt.children; 
       
   148         let spanslength = spans.length;
       
   149         // reduce accounted menu size according to jumps
       
   150         if(this.menu_offset != 0) spanslength--;
       
   151         if(this.menu_offset < contentlength - 1) spanslength--;
       
   152         if(forward){
       
   153             this.menu_offset = Math.min(
       
   154                 contentlength - spans.length + 1, 
       
   155                 this.menu_offset + spanslength);
       
   156         }else{
       
   157             this.menu_offset = Math.max(
       
   158                 0, 
       
   159                 this.menu_offset - spanslength);
       
   160         }
       
   161         this.set_partial_text();
       
   162     },
       
   163     // Setup partial view text content
       
   164     // with jumps at first and last entry when appropriate
       
   165     set_partial_text: function(){
       
   166         let spans = this.text_elt.children; 
       
   167         let contentlength = this.content.length;
       
   168         let spanslength = spans.length;
       
   169         let i = this.menu_offset, c = 0;
       
   170         while(c < spanslength){
       
   171             let span=spans[c];
       
   172             // backward jump only present if not exactly at start
       
   173             if(c == 0 && i != 0){
       
   174                 span.textContent = "↑  ↑  ↑";
       
   175                 span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_backward_click()");
       
   176             // presence of forward jump when not right at the end
       
   177             }else if(c == spanslength-1 && i < contentlength - 1){
       
   178                 span.textContent = "↓  ↓  ↓";
       
   179                 span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_forward_click()");
       
   180             // otherwise normal content
       
   181             }else{
       
   182                 span.textContent = this.content[i];
       
   183                 span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_selection_click("+i+")");
       
   184                 i++;
       
   185             }
       
   186             c++;
       
   187         }
       
   188     },
       
   189     open: function(){
       
   190         let length = this.content.length;
       
   191         // systematically reset text, to strip eventual whitespace spans
       
   192         this.reset_text();
       
   193         // grow as much as needed or possible
       
   194         let slots = this.grow_text(length);
       
   195         // Depending on final size
       
   196         if(slots == length) {
       
   197             // show all at once
       
   198             this.set_complete_text();
       
   199         } else {
       
   200             // eventualy align menu to current selection, compensating for lift
       
   201             let offset = this.last_selection - this.lift;
       
   202             if(offset > 0)
       
   203                 this.menu_offset = Math.min(offset + 1, length - slots + 1);
       
   204             else
       
   205                 this.menu_offset = 0;
       
   206             // show surrounding values
       
   207             this.set_partial_text();
       
   208         }
       
   209         // Now that text size is known, we can set the box around it
       
   210         this.adjust_box_to_text();
       
   211         // Take button out until menu closed
       
   212         this.element.removeChild(this.button_elt);
       
   213         // Rise widget to top by moving it to last position among siblings
       
   214         this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element));
       
   215         // disable interaction with background
       
   216         svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true);
       
   217         // mark as open
       
   218         this.opened = true;
       
   219     },
       
   220     // Put text element in normalized state
       
   221     reset_text: function(){
       
   222         let txt = this.text_elt; 
       
   223         let first = txt.firstElementChild;
       
   224         // remove attribute eventually added to first text line while opening
       
   225         first.removeAttribute("onclick");
       
   226         first.removeAttribute("dy");
       
   227         // keep only the first line of text
       
   228         for(let span of Array.from(txt.children).slice(1)){
       
   229             txt.removeChild(span)
       
   230         }
       
   231     },
       
   232     // Put rectangle element in saved original state
       
   233     reset_box: function(){
       
   234         let m = this.box_bbox;
       
   235         let b = this.box_elt;
       
   236         b.x.baseVal.value = m.x;
       
   237         b.y.baseVal.value = m.y;
       
   238         b.width.baseVal.value = m.width;
       
   239         b.height.baseVal.value = m.height;
       
   240     },
       
   241     // Use margin and text size to compute box size
       
   242     adjust_box_to_text: function(){
       
   243         let [lmargin, tmargin] = this.margins;
       
   244         let m = this.text_elt.getBBox();
       
   245         let b = this.box_elt;
       
   246         b.x.baseVal.value = m.x - lmargin;
       
   247         b.y.baseVal.value = m.y - tmargin;
       
   248         b.width.baseVal.value = 2 * lmargin + m.width;
       
   249         b.height.baseVal.value = 2 * tmargin + m.height;
       
   250     },
       
   251 ||
   261 ||
   252 }
   262 }