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