svghmi/widget_dropdown.ysl2
branchsvghmi
changeset 3091 f475f39713aa
parent 3090 9e172e4e50c7
child 3092 96ffd8b1b016
equal deleted inserted replaced
3090:9e172e4e50c7 3091:f475f39713aa
     1 // widget_dropdown.ysl2
     1 // widget_dropdown.ysl2
     2 
     2 
     3 template "widget[@type='DropDown']", mode="widget_class"{
     3 template "widget[@type='DropDown']", mode="widget_class"{
     4 ||
     4 ||
       
     5     function numb_event(e) {
       
     6         e.stopPropagation();
       
     7     }
     5     class DropDownWidget extends Widget{
     8     class DropDownWidget extends Widget{
     6         dispatch(value) {
     9         dispatch(value) {
     7             if(!this.opened) this.set_selection(value);
    10             if(!this.opened) this.set_selection(value);
     8         }
    11         }
     9         init() {
    12         init() {
    10             this.button_elt.onclick = this.on_button_click.bind(this);
    13             this.button_elt.onclick = this.on_button_click.bind(this);
    11             // Save original size of rectangle
    14             // Save original size of rectangle
    12             this.box_bbox = this.box_elt.getBBox()
    15             this.box_bbox = this.box_elt.getBBox()
    13 
    16 
    14             // Compute margins
    17             // Compute margins
    15             let text_bbox = this.text_elt.getBBox();
    18             this.text_bbox = this.text_elt.getBBox();
    16             let lmargin = text_bbox.x - this.box_bbox.x;
    19             let lmargin = this.text_bbox.x - this.box_bbox.x;
    17             let tmargin = text_bbox.y - this.box_bbox.y;
    20             let tmargin = this.text_bbox.y - this.box_bbox.y;
    18             this.margins = [lmargin, tmargin].map(x => Math.max(x,0));
    21             this.margins = [lmargin, tmargin].map(x => Math.max(x,0));
    19 
    22 
    20             // Index of first visible element in the menu, when opened
    23             // Index of first visible element in the menu, when opened
    21             this.menu_offset = 0;
    24             this.menu_offset = 0;
    22 
    25 
    28             this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this);
    31             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);
    32             this.bound_on_selection_click = this.on_selection_click.bind(this);
    30             this.bound_on_backward_click = this.on_backward_click.bind(this);
    33             this.bound_on_backward_click = this.on_backward_click.bind(this);
    31             this.bound_on_forward_click = this.on_forward_click.bind(this);
    34             this.bound_on_forward_click = this.on_forward_click.bind(this);
    32             this.opened = false;
    35             this.opened = false;
       
    36             this.clickables = [];
    33         }
    37         }
    34         on_button_click() {
    38         on_button_click() {
    35             this.open();
    39             this.open();
    36         }
    40         }
    37         // Called when a menu entry is clicked
    41         // Called when a menu entry is clicked
    59             // and contains selection when menu is closed
    63             // and contains selection when menu is closed
    60             this.text_elt.firstElementChild.textContent = display_str;
    64             this.text_elt.firstElementChild.textContent = display_str;
    61         }
    65         }
    62         grow_text(up_to) {
    66         grow_text(up_to) {
    63             let count = 1;
    67             let count = 1;
    64             let txt = this.text_elt; 
    68             let txt = this.text_elt;
    65             let first = txt.firstElementChild;
    69             let first = txt.firstElementChild;
    66             // Real world (pixels) boundaries of current page
    70             // Real world (pixels) boundaries of current page
    67             let bounds = svg_root.getBoundingClientRect(); 
    71             let bounds = svg_root.getBoundingClientRect();
    68             this.lift = 0;
    72             this.lift = 0;
    69             while(count < up_to) {
    73             while(count < up_to) {
    70                 let next = first.cloneNode();
    74                 let next = first.cloneNode();
    71                 // relative line by line text flow instead of absolute y coordinate
    75                 // relative line by line text flow instead of absolute y coordinate
    72                 next.removeAttribute("y");
    76                 next.removeAttribute("y");
   102             }
   106             }
   103             return count;
   107             return count;
   104         }
   108         }
   105         close_on_click_elsewhere(e) {
   109         close_on_click_elsewhere(e) {
   106             // inhibit events not targetting spans (menu items)
   110             // inhibit events not targetting spans (menu items)
   107             if(e.target.parentNode !== this.text_elt){
   111             if([this.text_elt, this.element].indexOf(e.target.parentNode) == -1){
   108                 e.stopPropagation();
   112                 e.stopPropagation();
   109                 // close menu in case click is outside box
   113                 // close menu in case click is outside box
   110                 if(e.target !== this.box_elt)
   114                 if(e.target !== this.box_elt)
   111                     this.close();
   115                     this.close();
   112             }
   116             }
   113         }
   117         }
   114         close(){
   118         close(){
   115             // Stop hogging all click events
   119             // Stop hogging all click events
       
   120             svg_root.removeEventListener("pointerdown", numb_event, true);
       
   121             svg_root.removeEventListener("pointerup", numb_event, true);
   116             svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true);
   122             svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true);
   117             // Restore position and sixe of widget elements
   123             // Restore position and sixe of widget elements
   118             this.reset_text();
   124             this.reset_text();
       
   125             this.reset_clickables();
   119             this.reset_box();
   126             this.reset_box();
   120             // Put the button back in place
   127             // Put the button back in place
   121             this.element.appendChild(this.button_elt);
   128             this.element.appendChild(this.button_elt);
   122             // Mark as closed (to allow dispatch)
   129             // Mark as closed (to allow dispatch)
   123             this.opened = false;
   130             this.opened = false;
   124             // Dispatch last cached value
   131             // Dispatch last cached value
   125             this.apply_cache();
   132             this.apply_cache();
   126         }
   133         }
       
   134         // Make item (text span) clickable by overlaying a rectangle on top of it
       
   135         make_clickable(span, func) {
       
   136             let txt = this.text_elt;
       
   137             let first = txt.firstElementChild;
       
   138             let original_text_y = this.text_bbox.y;
       
   139             let highlight = this.highlight_elt;
       
   140             let original_h_y = highlight.getBBox().y;
       
   141             let clickable = highlight.cloneNode();
       
   142             let yoffset = span.getBBox().y - original_text_y;
       
   143             clickable.setAttribute("y", original_h_y + yoffset); 
       
   144             clickable.style.pointerEvents = "bounding-box";
       
   145             clickable.style.visibility = "hidden";
       
   146             //clickable.onclick = () => alert("love JS");
       
   147             clickable.onclick = func;
       
   148             this.element.appendChild(clickable);
       
   149             this.clickables.push(clickable)
       
   150         }
       
   151         reset_clickables() {
       
   152             while(this.clickables.length){
       
   153                 this.element.removeChild(this.clickables.pop());
       
   154             }
       
   155         }
   127         // Set text content when content is smaller than menu (no scrolling)
   156         // Set text content when content is smaller than menu (no scrolling)
   128         set_complete_text(){
   157         set_complete_text(){
   129             let spans = this.text_elt.children; 
   158             let spans = this.text_elt.children;
   130             let c = 0;
   159             let c = 0;
   131             for(let item of this.content){
   160             for(let item of this.content){
   132                 let span=spans[c];
   161                 let span=spans[c];
   133                 span.textContent = item;
   162                 span.textContent = item;
   134                 let sel = c;
   163                 let sel = c;
   135                 span.onclick = (evt) => this.bound_on_selection_click(sel);
   164                 this.make_clickable(span, (evt) => this.bound_on_selection_click(sel));
   136                 c++;
   165                 c++;
   137             }
   166             }
   138         }
   167         }
   139         // Move partial view :
   168         // Move partial view :
   140         // false : upward, lower value
   169         // false : upward, lower value
   141         // true  : downward, higher value
   170         // true  : downward, higher value
   142         scroll(forward){
   171         scroll(forward){
   143             let contentlength = this.content.length;
   172             let contentlength = this.content.length;
   144             let spans = this.text_elt.children; 
   173             let spans = this.text_elt.children;
   145             let spanslength = spans.length;
   174             let spanslength = spans.length;
   146             // reduce accounted menu size according to jumps
   175             // reduce accounted menu size according to jumps
   147             if(this.menu_offset != 0) spanslength--;
   176             if(this.menu_offset != 0) spanslength--;
   148             if(this.menu_offset < contentlength - 1) spanslength--;
   177             if(this.menu_offset < contentlength - 1) spanslength--;
   149             if(forward){
   178             if(forward){
   150                 this.menu_offset = Math.min(
   179                 this.menu_offset = Math.min(
   151                     contentlength - spans.length + 1, 
   180                     contentlength - spans.length + 1,
   152                     this.menu_offset + spanslength);
   181                     this.menu_offset + spanslength);
   153             }else{
   182             }else{
   154                 this.menu_offset = Math.max(
   183                 this.menu_offset = Math.max(
   155                     0, 
   184                     0,
   156                     this.menu_offset - spanslength);
   185                     this.menu_offset - spanslength);
   157             }
   186             }
       
   187             this.reset_clickables();
   158             this.set_partial_text();
   188             this.set_partial_text();
   159         }
   189         }
   160         // Setup partial view text content
   190         // Setup partial view text content
   161         // with jumps at first and last entry when appropriate
   191         // with jumps at first and last entry when appropriate
   162         set_partial_text(){
   192         set_partial_text(){
   163             let spans = this.text_elt.children; 
   193             let spans = this.text_elt.children;
   164             let contentlength = this.content.length;
   194             let contentlength = this.content.length;
   165             let spanslength = spans.length;
   195             let spanslength = spans.length;
   166             let i = this.menu_offset, c = 0;
   196             let i = this.menu_offset, c = 0;
       
   197             let m = this.box_bbox;
   167             while(c < spanslength){
   198             while(c < spanslength){
   168                 let span=spans[c];
   199                 let span=spans[c];
       
   200                 let onclickfunc;
   169                 // backward jump only present if not exactly at start
   201                 // backward jump only present if not exactly at start
   170                 if(c == 0 && i != 0){
   202                 if(c == 0 && i != 0){
   171                     span.textContent = "↑  ↑  ↑";
   203                     span.textContent = "▲";
   172                     span.onclick = this.bound_on_backward_click;
   204                     onclickfunc = this.bound_on_backward_click;
       
   205                     let o = span.getBBox();
       
   206                     span.setAttribute("dx", (m.width - o.width)/2);
   173                 // presence of forward jump when not right at the end
   207                 // presence of forward jump when not right at the end
   174                 }else if(c == spanslength-1 && i < contentlength - 1){
   208                 }else if(c == spanslength-1 && i < contentlength - 1){
   175                     span.textContent = "↓  ↓  ↓";
   209                     span.textContent = "▼";
   176                     span.onclick = this.bound_on_forward_click;
   210                     onclickfunc = this.bound_on_forward_click;
       
   211                     let o = span.getBBox();
       
   212                     span.setAttribute("dx", (m.width - o.width)/2);
   177                 // otherwise normal content
   213                 // otherwise normal content
   178                 }else{
   214                 }else{
   179                     span.textContent = this.content[i];
   215                     span.textContent = this.content[i];
   180                     let sel = i;
   216                     let sel = i;
   181                     span.onclick = (evt) => this.bound_on_selection_click(sel);
   217                     onclickfunc = (evt) => this.bound_on_selection_click(sel);
       
   218                     span.removeAttribute("dx");
   182                     i++;
   219                     i++;
   183                 }
   220                 }
       
   221                 this.make_clickable(span, onclickfunc);
   184                 c++;
   222                 c++;
   185             }
   223             }
   186         }
   224         }
   187         open(){
   225         open(){
   188             let length = this.content.length;
   226             let length = this.content.length;
   209             // Take button out until menu closed
   247             // Take button out until menu closed
   210             this.element.removeChild(this.button_elt);
   248             this.element.removeChild(this.button_elt);
   211             // Rise widget to top by moving it to last position among siblings
   249             // Rise widget to top by moving it to last position among siblings
   212             this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element));
   250             this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element));
   213             // disable interaction with background
   251             // disable interaction with background
       
   252             svg_root.addEventListener("pointerdown", numb_event, true);
       
   253             svg_root.addEventListener("pointerup", numb_event, true);
   214             svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true);
   254             svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true);
   215             // mark as open
   255             // mark as open
   216             this.opened = true;
   256             this.opened = true;
   217         }
   257         }
   218         // Put text element in normalized state
   258         // Put text element in normalized state
   219         reset_text(){
   259         reset_text(){
   220             let txt = this.text_elt; 
   260             let txt = this.text_elt;
   221             let first = txt.firstElementChild;
   261             let first = txt.firstElementChild;
   222             // remove attribute eventually added to first text line while opening
   262             // remove attribute eventually added to first text line while opening
   223             first.removeAttribute("onclick");
   263             first.onclick = null;
   224             first.removeAttribute("dy");
   264             first.removeAttribute("dy");
       
   265             first.removeAttribute("dx");
   225             // keep only the first line of text
   266             // keep only the first line of text
   226             for(let span of Array.from(txt.children).slice(1)){
   267             for(let span of Array.from(txt.children).slice(1)){
   227                 txt.removeChild(span)
   268                 txt.removeChild(span)
   228             }
   269             }
   229         }
   270         }
   239         // Use margin and text size to compute box size
   280         // Use margin and text size to compute box size
   240         adjust_box_to_text(){
   281         adjust_box_to_text(){
   241             let [lmargin, tmargin] = this.margins;
   282             let [lmargin, tmargin] = this.margins;
   242             let m = this.text_elt.getBBox();
   283             let m = this.text_elt.getBBox();
   243             let b = this.box_elt;
   284             let b = this.box_elt;
   244             b.x.baseVal.value = m.x - lmargin;
   285             // b.x.baseVal.value = m.x - lmargin;
   245             b.y.baseVal.value = m.y - tmargin;
   286             b.y.baseVal.value = m.y - tmargin;
   246             b.width.baseVal.value = 2 * lmargin + m.width;
   287             // b.width.baseVal.value = 2 * lmargin + m.width;
   247             b.height.baseVal.value = 2 * tmargin + m.height;
   288             b.height.baseVal.value = 2 * tmargin + m.height;
   248         }
   289         }
   249     }
   290     }
   250 ||
   291 ||
   251 }
   292 }
       
   293 
   252 template "widget[@type='DropDown']", mode="widget_defs" {
   294 template "widget[@type='DropDown']", mode="widget_defs" {
   253     param "hmi_element";
   295     param "hmi_element";
   254     labels("text box button");
   296     labels("text box button highlight");
   255 ||
   297 ||
   256     // It is assumed that list content conforms to Array interface.
   298     // It is assumed that list content conforms to Array interface.
   257     content: [
   299     content: [
   258     ``foreach "arg" | "«@value»",
   300     ``foreach "arg" | "«@value»",
   259     ],
   301     ],