Edouard@2922: // widget_dropdown.ysl2 Edouard@2922: Edouard@2922: template "widget[@type='DropDown']", mode="widget_defs" { Edouard@2922: param "hmi_element"; edouard@2926: labels("text box button"); Edouard@2935: || edouard@2923: dispatch: function(value) { edouard@2924: if(!this.opened) this.set_selection(value); edouard@2923: }, edouard@2923: init: function() { edouard@2926: this.button_elt.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_button_click()"); edouard@2923: this.text_bbox = this.text_elt.getBBox() edouard@2923: this.box_bbox = this.box_elt.getBBox() edouard@2923: lmargin = this.text_bbox.x - this.box_bbox.x; edouard@2923: tmargin = this.text_bbox.y - this.box_bbox.y; edouard@2926: this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); Edouard@2935: Edouard@2935: // It is assumed that list content conforms to Array interface. edouard@2931: this.content = [ edouard@2931: ``foreach "arg" | "«@value»", edouard@2931: ]; Edouard@2935: Edouard@2935: // Index of first visible element in the menu, when opened edouard@2924: this.menu_offset = 0; Edouard@2935: Edouard@2935: // How mutch to lift the menu vertically so that it does not cross bottom border edouard@2923: this.lift = 0; Edouard@2935: Edouard@2935: // Event handlers cannot be object method ('this' is unknown) Edouard@2935: // as a workaround, handler given to addEventListener is bound in advance. Edouard@2935: this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this); Edouard@2935: edouard@2924: this.opened = false; Edouard@2935: }, Edouard@2935: // Called when a menu entry is clicked edouard@2924: on_selection_click: function(selection) { edouard@2926: this.close(); edouard@2930: let orig = this.indexes[0]; edouard@2930: let idx = this.offset ? orig - this.offset : orig; edouard@2930: apply_hmi_value(idx, selection); edouard@2923: }, edouard@2926: on_button_click: function() { edouard@2926: this.open(); edouard@2923: }, Edouard@2935: on_backward_click: function(){ Edouard@2935: this.scroll(false); edouard@2925: }, edouard@2925: on_forward_click:function(){ Edouard@2935: this.scroll(true); edouard@2925: }, edouard@2924: set_selection: function(value) { edouard@2934: let display_str; edouard@2934: if(value >= 0 && value < this.content.length){ Edouard@2935: // if valid selection resolve content edouard@2934: display_str = this.content[value]; edouard@2934: this.last_selection = value; edouard@2934: } else { Edouard@2935: // otherwise show problem edouard@2934: display_str = "?"+String(value)+"?"; edouard@2934: } Edouard@2935: // It is assumed that first span always stays, Edouard@2935: // and contains selection when menu is closed edouard@2934: this.text_elt.firstElementChild.textContent = display_str; edouard@2924: }, edouard@2924: grow_text: function(up_to) { edouard@2924: let count = 1; edouard@2923: let txt = this.text_elt; edouard@2923: let first = txt.firstElementChild; Edouard@2935: // Real world (pixels) boundaries of current page edouard@2923: let bounds = svg_root.getBoundingClientRect(); edouard@2924: this.lift = 0; edouard@2924: while(count < up_to) { edouard@2924: let next = first.cloneNode(); Edouard@2935: // relative line by line text flow instead of absolute y coordinate edouard@2924: next.removeAttribute("y"); edouard@2924: next.setAttribute("dy", "1.1em"); Edouard@2935: // default content to allow computing text element bbox edouard@2924: next.textContent = "..."; Edouard@2935: // append new span to text element edouard@2924: txt.appendChild(next); Edouard@2935: // now check if text extended by one row fits to page Edouard@2935: // FIXME : exclude margins to be more accurate on box size edouard@2924: let rect = txt.getBoundingClientRect(); edouard@2924: if(rect.bottom > bounds.bottom){ Edouard@2935: // in case of overflow at the bottom, lift up one row edouard@2924: let backup = first.getAttribute("dy"); Edouard@2935: // apply lift asr a dy added too first span (y attrib stays) edouard@2924: first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em"); edouard@2924: rect = txt.getBoundingClientRect(); edouard@2924: if(rect.top > bounds.top){ edouard@2924: this.lift += 1; edouard@2924: } else { Edouard@2935: // if it goes over the top, then backtrack Edouard@2935: // restore dy attribute on first span edouard@2924: if(backup) edouard@2924: first.setAttribute("dy", backup); edouard@2924: else edouard@2924: first.removeAttribute("dy"); Edouard@2935: // remove unwanted child edouard@2924: txt.removeChild(next); edouard@2924: return count; edouard@2924: } edouard@2923: } edouard@2924: count++; edouard@2923: } edouard@2924: return count; edouard@2924: }, edouard@2933: close_on_click_elsewhere: function(e) { Edouard@2935: // inhibit events not targetting spans (menu items) edouard@2933: if(e.target.parentNode !== this.text_elt){ edouard@2927: e.stopPropagation(); Edouard@2935: // close menu in case click is outside box edouard@2933: if(e.target !== this.box_elt) edouard@2933: this.close(); edouard@2933: } edouard@2927: }, edouard@2924: close: function(){ Edouard@2935: // Stop hogging all click events Edouard@2935: svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); Edouard@2935: // Restore position and sixe of widget elements edouard@2924: this.reset_text(); edouard@2924: this.reset_box(); Edouard@2935: // Put the button back in place edouard@2926: this.element.appendChild(this.button_elt); Edouard@2935: // Mark as closed (to allow dispatch) edouard@2932: this.opened = false; Edouard@2935: // Dispatch last cached value edouard@2930: this.apply_cache(); edouard@2924: }, Edouard@2935: // Set text content when content is smaller than menu (no scrolling) edouard@2924: set_complete_text: function(){ edouard@2924: let spans = this.text_elt.children; edouard@2924: let c = 0; edouard@2924: for(let item of this.content){ edouard@2924: let span=spans[c]; edouard@2924: span.textContent = item; edouard@2924: span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_selection_click("+c+")"); edouard@2924: c++; edouard@2924: } edouard@2924: }, Edouard@2935: // Move partial view : Edouard@2935: // false : upward, lower value Edouard@2935: // true : downward, higher value Edouard@2935: scroll: function(forward){ edouard@2925: let contentlength = this.content.length; edouard@2925: let spans = this.text_elt.children; edouard@2925: let spanslength = spans.length; edouard@2925: if(this.menu_offset != 0) spanslength--; edouard@2925: if(this.menu_offset < contentlength - 1) spanslength--; edouard@2925: if(forward){ edouard@2925: this.menu_offset = Math.min( edouard@2925: contentlength - spans.length + 1, edouard@2925: this.menu_offset + spanslength); edouard@2925: }else{ edouard@2925: this.menu_offset = Math.max( edouard@2925: 0, edouard@2925: this.menu_offset - spanslength); edouard@2925: } edouard@2925: console.log(this.menu_offset); edouard@2925: this.set_partial_text(); edouard@2925: }, Edouard@2935: // Setup partial view text content Edouard@2935: // with jumps at first and last entry when appropriate edouard@2924: set_partial_text: function(){ edouard@2924: let spans = this.text_elt.children; edouard@2925: let contentlength = this.content.length; edouard@2925: let spanslength = spans.length; edouard@2924: let i = this.menu_offset, c = 0; edouard@2925: while(c < spanslength){ edouard@2925: let span=spans[c]; edouard@2924: if(c == 0 && i != 0){ edouard@2925: span.textContent = "↑ ↑ ↑"; edouard@2925: span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_backward_click()"); edouard@2925: }else if(c == spanslength-1 && i < contentlength - 1){ edouard@2925: span.textContent = "↓ ↓ ↓"; edouard@2925: span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_forward_click()"); edouard@2925: }else{ edouard@2924: span.textContent = this.content[i]; edouard@2924: span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_selection_click("+i+")"); edouard@2924: i++; edouard@2924: } edouard@2924: c++; edouard@2924: } edouard@2923: }, edouard@2923: open: function(){ edouard@2924: let length = this.content.length; Edouard@2935: // systematically reset text, to strip eventual whitespace spans edouard@2924: this.reset_text(); Edouard@2935: // grow as much as needed or possible edouard@2924: let slots = this.grow_text(length); Edouard@2935: // Depending on final size edouard@2924: if(slots == length) { Edouard@2935: // show all at once edouard@2924: this.set_complete_text(); edouard@2923: } else { Edouard@2935: // eventualy align menu to current selection, compensating for lift edouard@2934: let offset = this.last_selection - this.lift; edouard@2934: if(offset > 0) edouard@2934: this.menu_offset = Math.min(offset + 1, length - slots + 1); edouard@2934: else edouard@2934: this.menu_offset = 0; Edouard@2935: // show surrounding values edouard@2924: this.set_partial_text(); edouard@2923: } Edouard@2935: // Now that text size is known, we can set the box around it edouard@2924: this.adjust_box_to_text(); Edouard@2935: // Take button out until menu closed edouard@2926: this.element.removeChild(this.button_elt); Edouard@2935: // Place widget in front by moving it to last position among siblings edouard@2928: this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); edouard@2927: // disable interaction with background Edouard@2935: svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); edouard@2924: this.opened = true; edouard@2923: }, edouard@2924: reset_text: function(){ edouard@2923: let txt = this.text_elt; edouard@2924: let first = txt.firstElementChild; edouard@2924: first.removeAttribute("onclick"); edouard@2924: first.removeAttribute("dy"); edouard@2923: for(let span of Array.from(txt.children).slice(1)){ edouard@2923: txt.removeChild(span) edouard@2923: } edouard@2923: }, edouard@2924: reset_box: function(){ edouard@2924: let m = this.box_bbox; edouard@2924: let b = this.box_elt; edouard@2924: b.x.baseVal.value = m.x; edouard@2924: b.y.baseVal.value = m.y; edouard@2924: b.width.baseVal.value = m.width; edouard@2924: b.height.baseVal.value = m.height; edouard@2924: }, edouard@2924: adjust_box_to_text: function(){ edouard@2926: let [lmargin, tmargin] = this.margins; edouard@2923: let m = this.text_elt.getBBox(); edouard@2924: let b = this.box_elt; edouard@2924: b.x.baseVal.value = m.x - lmargin; edouard@2924: b.y.baseVal.value = m.y - tmargin; edouard@2926: b.width.baseVal.value = 2 * lmargin + m.width; edouard@2926: b.height.baseVal.value = 2 * tmargin + m.height; edouard@2923: }, edouard@2923: || Edouard@2922: }