--- a/svghmi/gen_index_xhtml.xslt Thu Dec 10 15:48:15 2020 +0100
+++ b/svghmi/gen_index_xhtml.xslt Tue Dec 15 13:43:21 2020 +0100
@@ -1901,8 +1901,6 @@
<xsl:with-param name="mandatory" select="'no'"/>
<xsl:template mode="widget_class" match="widget[@type='CircularBar']">
<xsl:text>class CircularBarWidget extends Widget{
@@ -3063,6 +3061,496 @@
+ <xsl:template mode="widget_class" match="widget[@type='DropDown']"> + <xsl:text> class DropDownWidget extends Widget{ + <xsl:text> dispatch(value) { + <xsl:text> if(!this.opened) this.set_selection(value); + <xsl:text> this.button_elt.onclick = this.on_button_click.bind(this); + <xsl:text> // Save original size of rectangle + <xsl:text> this.box_bbox = this.box_elt.getBBox() + <xsl:text> // Compute margins + <xsl:text> let text_bbox = this.text_elt.getBBox(); + <xsl:text> let lmargin = text_bbox.x - this.box_bbox.x; + <xsl:text> let tmargin = text_bbox.y - this.box_bbox.y; + <xsl:text> this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); + <xsl:text> // Index of first visible element in the menu, when opened + <xsl:text> this.menu_offset = 0; + <xsl:text> // How mutch to lift the menu vertically so that it does not cross bottom border + <xsl:text> this.lift = 0; + <xsl:text> // Event handlers cannot be object method ('this' is unknown) + <xsl:text> // as a workaround, handler given to addEventListener is bound in advance. + <xsl:text> this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this); + <xsl:text> this.bound_on_selection_click = this.on_selection_click.bind(this); + <xsl:text> this.bound_on_backward_click = this.on_backward_click.bind(this); + <xsl:text> this.bound_on_forward_click = this.on_forward_click.bind(this); + <xsl:text> this.opened = false; + <xsl:text> on_button_click() { + <xsl:text> this.open(); + <xsl:text> // Called when a menu entry is clicked + <xsl:text> on_selection_click(selection) { + <xsl:text> this.close(); + <xsl:text> this.apply_hmi_value(0, selection); + <xsl:text> on_backward_click(){ + <xsl:text> this.scroll(false); + <xsl:text> on_forward_click(){ + <xsl:text> this.scroll(true); + <xsl:text> set_selection(value) { + <xsl:text> let display_str; + <xsl:text> if(value >= 0 && value < this.content.length){ + <xsl:text> // if valid selection resolve content + <xsl:text> display_str = this.content[value]; + <xsl:text> this.last_selection = value; + <xsl:text> // otherwise show problem + <xsl:text> display_str = "?"+String(value)+"?"; + <xsl:text> // It is assumed that first span always stays, + <xsl:text> // and contains selection when menu is closed + <xsl:text> this.text_elt.firstElementChild.textContent = display_str; + <xsl:text> grow_text(up_to) { + <xsl:text> let count = 1; + <xsl:text> let txt = this.text_elt; + <xsl:text> let first = txt.firstElementChild; + <xsl:text> // Real world (pixels) boundaries of current page + <xsl:text> let bounds = svg_root.getBoundingClientRect(); + <xsl:text> this.lift = 0; + <xsl:text> while(count < up_to) { + <xsl:text> let next = first.cloneNode(); + <xsl:text> // relative line by line text flow instead of absolute y coordinate + <xsl:text> next.removeAttribute("y"); + <xsl:text> next.setAttribute("dy", "1.1em"); + <xsl:text> // default content to allow computing text element bbox + <xsl:text> next.textContent = "..."; + <xsl:text> // append new span to text element + <xsl:text> txt.appendChild(next); + <xsl:text> // now check if text extended by one row fits to page + <xsl:text> // FIXME : exclude margins to be more accurate on box size + <xsl:text> let rect = txt.getBoundingClientRect(); + <xsl:text> if(rect.bottom > bounds.bottom){ + <xsl:text> // in case of overflow at the bottom, lift up one row + <xsl:text> let backup = first.getAttribute("dy"); + <xsl:text> // apply lift asr a dy added too first span (y attrib stays) + <xsl:text> first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em"); + <xsl:text> rect = txt.getBoundingClientRect(); + <xsl:text> if(rect.top > bounds.top){ + <xsl:text> this.lift += 1; + <xsl:text> // if it goes over the top, then backtrack + <xsl:text> // restore dy attribute on first span + <xsl:text> first.setAttribute("dy", backup); + <xsl:text> first.removeAttribute("dy"); + <xsl:text> // remove unwanted child + <xsl:text> txt.removeChild(next); + <xsl:text> return count; + <xsl:text> return count; + <xsl:text> close_on_click_elsewhere(e) { + <xsl:text> // inhibit events not targetting spans (menu items) + <xsl:text> if(e.target.parentNode !== this.text_elt){ + <xsl:text> e.stopPropagation(); + <xsl:text> // close menu in case click is outside box + <xsl:text> if(e.target !== this.box_elt) + <xsl:text> this.close(); + <xsl:text> // Stop hogging all click events + <xsl:text> svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); + <xsl:text> // Restore position and sixe of widget elements + <xsl:text> this.reset_text(); + <xsl:text> this.reset_box(); + <xsl:text> // Put the button back in place + <xsl:text> this.element.appendChild(this.button_elt); + <xsl:text> // Mark as closed (to allow dispatch) + <xsl:text> this.opened = false; + <xsl:text> // Dispatch last cached value + <xsl:text> this.apply_cache(); + <xsl:text> // Set text content when content is smaller than menu (no scrolling) + <xsl:text> set_complete_text(){ + <xsl:text> let spans = this.text_elt.children; + <xsl:text> for(let item of this.content){ + <xsl:text> let span=spans[c]; + <xsl:text> span.textContent = item; + <xsl:text> span.onclick = (evt) => this.bound_on_selection_click(c); + <xsl:text> // Move partial view : + <xsl:text> // false : upward, lower value + <xsl:text> // true : downward, higher value + <xsl:text> scroll(forward){ + <xsl:text> let contentlength = this.content.length; + <xsl:text> let spans = this.text_elt.children; + <xsl:text> let spanslength = spans.length; + <xsl:text> // reduce accounted menu size according to jumps + <xsl:text> if(this.menu_offset != 0) spanslength--; + <xsl:text> if(this.menu_offset < contentlength - 1) spanslength--; + <xsl:text> if(forward){ + <xsl:text> this.menu_offset = Math.min( + <xsl:text> contentlength - spans.length + 1, + <xsl:text> this.menu_offset + spanslength); + <xsl:text> this.menu_offset = Math.max( + <xsl:text> this.menu_offset - spanslength); + <xsl:text> this.set_partial_text(); + <xsl:text> // Setup partial view text content + <xsl:text> // with jumps at first and last entry when appropriate + <xsl:text> set_partial_text(){ + <xsl:text> let spans = this.text_elt.children; + <xsl:text> let contentlength = this.content.length; + <xsl:text> let spanslength = spans.length; + <xsl:text> let i = this.menu_offset, c = 0; + <xsl:text> while(c < spanslength){ + <xsl:text> let span=spans[c]; + <xsl:text> // backward jump only present if not exactly at start + <xsl:text> if(c == 0 && i != 0){ + <xsl:text> span.textContent = "↑ ↑ ↑"; + <xsl:text> span.onclick = this.bound_on_backward_click; + <xsl:text> // presence of forward jump when not right at the end + <xsl:text> }else if(c == spanslength-1 && i < contentlength - 1){ + <xsl:text> span.textContent = "↓ ↓ ↓"; + <xsl:text> span.onclick = this.bound_on_forward_click; + <xsl:text> // otherwise normal content + <xsl:text> span.textContent = this.content[i]; + <xsl:text> let sel = i; + <xsl:text> span.onclick = (evt) => this.bound_on_selection_click(sel); + <xsl:text> let length = this.content.length; + <xsl:text> // systematically reset text, to strip eventual whitespace spans + <xsl:text> this.reset_text(); + <xsl:text> // grow as much as needed or possible + <xsl:text> let slots = this.grow_text(length); + <xsl:text> // Depending on final size + <xsl:text> if(slots == length) { + <xsl:text> // show all at once + <xsl:text> this.set_complete_text(); + <xsl:text> // eventualy align menu to current selection, compensating for lift + <xsl:text> let offset = this.last_selection - this.lift; + <xsl:text> if(offset > 0) + <xsl:text> this.menu_offset = Math.min(offset + 1, length - slots + 1); + <xsl:text> this.menu_offset = 0; + <xsl:text> // show surrounding values + <xsl:text> this.set_partial_text(); + <xsl:text> // Now that text size is known, we can set the box around it + <xsl:text> this.adjust_box_to_text(); + <xsl:text> // Take button out until menu closed + <xsl:text> this.element.removeChild(this.button_elt); + <xsl:text> // Rise widget to top by moving it to last position among siblings + <xsl:text> this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); + <xsl:text> // disable interaction with background + <xsl:text> svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); + <xsl:text> // mark as open + <xsl:text> this.opened = true; + <xsl:text> // Put text element in normalized state + <xsl:text> reset_text(){ + <xsl:text> let txt = this.text_elt; + <xsl:text> let first = txt.firstElementChild; + <xsl:text> // remove attribute eventually added to first text line while opening + <xsl:text> first.removeAttribute("onclick"); + <xsl:text> first.removeAttribute("dy"); + <xsl:text> // keep only the first line of text + <xsl:text> for(let span of Array.from(txt.children).slice(1)){ + <xsl:text> txt.removeChild(span) + <xsl:text> // Put rectangle element in saved original state + <xsl:text> reset_box(){ + <xsl:text> let m = this.box_bbox; + <xsl:text> let b = this.box_elt; + <xsl:text> b.x.baseVal.value = m.x; + <xsl:text> b.y.baseVal.value = m.y; + <xsl:text> b.width.baseVal.value = m.width; + <xsl:text> b.height.baseVal.value = m.height; + <xsl:text> // Use margin and text size to compute box size + <xsl:text> adjust_box_to_text(){ + <xsl:text> let [lmargin, tmargin] = this.margins; + <xsl:text> let m = this.text_elt.getBBox(); + <xsl:text> let b = this.box_elt; + <xsl:text> b.x.baseVal.value = m.x - lmargin; + <xsl:text> b.y.baseVal.value = m.y - tmargin; + <xsl:text> b.width.baseVal.value = 2 * lmargin + m.width; + <xsl:text> b.height.baseVal.value = 2 * tmargin + m.height; <xsl:template mode="widget_defs" match="widget[@type='DropDown']">
<xsl:param name="hmi_element"/>
<xsl:call-template name="defs_by_labels">
@@ -3071,39 +3559,9 @@
<xsl:text>text box button</xsl:text>
- <xsl:text> dispatch: function(value) {
- <xsl:text> if(!this.opened) this.set_selection(value);
- <xsl:text> init: function() {
- <xsl:text> this.button_elt.setAttribute("onclick", "hmi_widgets['</xsl:text>
- <xsl:value-of select="$hmi_element/@id"/>
- <xsl:text>'].on_button_click()");
- <xsl:text> // Save original size of rectangle
- <xsl:text> this.box_bbox = this.box_elt.getBBox()
- <xsl:text> // Compute margins
- <xsl:text> text_bbox = this.text_elt.getBBox()
- <xsl:text> lmargin = text_bbox.x - this.box_bbox.x;
- <xsl:text> tmargin = text_bbox.y - this.box_bbox.y;
- <xsl:text> this.margins = [lmargin, tmargin].map(x => Math.max(x,0));
- <xsl:text> // It is assumed that list content conforms to Array interface.
- <xsl:text> this.content = [
+ <xsl:text> // It is assumed that list content conforms to Array interface. <xsl:for-each select="arg">
@@ -3111,467 +3569,9 @@
- <xsl:text> // Index of first visible element in the menu, when opened
- <xsl:text> this.menu_offset = 0;
- <xsl:text> // How mutch to lift the menu vertically so that it does not cross bottom border
- <xsl:text> this.lift = 0;
- <xsl:text> // Event handlers cannot be object method ('this' is unknown)
- <xsl:text> // as a workaround, handler given to addEventListener is bound in advance.
- <xsl:text> this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this);
- <xsl:text> this.opened = false;
- <xsl:text> // Called when a menu entry is clicked
- <xsl:text> on_selection_click: function(selection) {
- <xsl:text> this.close();
- <xsl:text> this.apply_hmi_value(0, selection);
- <xsl:text> on_button_click: function() {
- <xsl:text> this.open();
- <xsl:text> on_backward_click: function(){
- <xsl:text> this.scroll(false);
- <xsl:text> on_forward_click:function(){
- <xsl:text> this.scroll(true);
- <xsl:text> set_selection: function(value) {
- <xsl:text> let display_str;
- <xsl:text> if(value >= 0 && value < this.content.length){
- <xsl:text> // if valid selection resolve content
- <xsl:text> display_str = this.content[value];
- <xsl:text> this.last_selection = value;
- <xsl:text> // otherwise show problem
- <xsl:text> display_str = "?"+String(value)+"?";
- <xsl:text> // It is assumed that first span always stays,
- <xsl:text> // and contains selection when menu is closed
- <xsl:text> this.text_elt.firstElementChild.textContent = display_str;
- <xsl:text> grow_text: function(up_to) {
- <xsl:text> let count = 1;
- <xsl:text> let txt = this.text_elt;
- <xsl:text> let first = txt.firstElementChild;
- <xsl:text> // Real world (pixels) boundaries of current page
- <xsl:text> let bounds = svg_root.getBoundingClientRect();
- <xsl:text> this.lift = 0;
- <xsl:text> while(count < up_to) {
- <xsl:text> let next = first.cloneNode();
- <xsl:text> // relative line by line text flow instead of absolute y coordinate
- <xsl:text> next.removeAttribute("y");
- <xsl:text> next.setAttribute("dy", "1.1em");
- <xsl:text> // default content to allow computing text element bbox
- <xsl:text> next.textContent = "...";
- <xsl:text> // append new span to text element
- <xsl:text> txt.appendChild(next);
- <xsl:text> // now check if text extended by one row fits to page
- <xsl:text> // FIXME : exclude margins to be more accurate on box size
- <xsl:text> let rect = txt.getBoundingClientRect();
- <xsl:text> if(rect.bottom > bounds.bottom){
- <xsl:text> // in case of overflow at the bottom, lift up one row
- <xsl:text> let backup = first.getAttribute("dy");
- <xsl:text> // apply lift asr a dy added too first span (y attrib stays)
- <xsl:text> first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em");
- <xsl:text> rect = txt.getBoundingClientRect();
- <xsl:text> if(rect.top > bounds.top){
- <xsl:text> this.lift += 1;
- <xsl:text> // if it goes over the top, then backtrack
- <xsl:text> // restore dy attribute on first span
- <xsl:text> first.setAttribute("dy", backup);
- <xsl:text> first.removeAttribute("dy");
- <xsl:text> // remove unwanted child
- <xsl:text> txt.removeChild(next);
- <xsl:text> return count;
- <xsl:text> return count;
- <xsl:text> close_on_click_elsewhere: function(e) {
- <xsl:text> // inhibit events not targetting spans (menu items)
- <xsl:text> if(e.target.parentNode !== this.text_elt){
- <xsl:text> e.stopPropagation();
- <xsl:text> // close menu in case click is outside box
- <xsl:text> if(e.target !== this.box_elt)
- <xsl:text> this.close();
- <xsl:text> close: function(){
- <xsl:text> // Stop hogging all click events
- <xsl:text> svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true);
- <xsl:text> // Restore position and sixe of widget elements
- <xsl:text> this.reset_text();
- <xsl:text> this.reset_box();
- <xsl:text> // Put the button back in place
- <xsl:text> this.element.appendChild(this.button_elt);
- <xsl:text> // Mark as closed (to allow dispatch)
- <xsl:text> this.opened = false;
- <xsl:text> // Dispatch last cached value
- <xsl:text> this.apply_cache();
- <xsl:text> // Set text content when content is smaller than menu (no scrolling)
- <xsl:text> set_complete_text: function(){
- <xsl:text> let spans = this.text_elt.children;
- <xsl:text> for(let item of this.content){
- <xsl:text> let span=spans[c];
- <xsl:text> span.textContent = item;
- <xsl:text> span.setAttribute("onclick", "hmi_widgets['</xsl:text>
- <xsl:value-of select="$hmi_element/@id"/>
- <xsl:text>'].on_selection_click("+c+")");
- <xsl:text> // Move partial view :
- <xsl:text> // false : upward, lower value
- <xsl:text> // true : downward, higher value
- <xsl:text> scroll: function(forward){
- <xsl:text> let contentlength = this.content.length;
- <xsl:text> let spans = this.text_elt.children;
- <xsl:text> let spanslength = spans.length;
- <xsl:text> // reduce accounted menu size according to jumps
- <xsl:text> if(this.menu_offset != 0) spanslength--;
- <xsl:text> if(this.menu_offset < contentlength - 1) spanslength--;
- <xsl:text> if(forward){
- <xsl:text> this.menu_offset = Math.min(
- <xsl:text> contentlength - spans.length + 1,
- <xsl:text> this.menu_offset + spanslength);
- <xsl:text> this.menu_offset = Math.max(
- <xsl:text> this.menu_offset - spanslength);
- <xsl:text> this.set_partial_text();
- <xsl:text> // Setup partial view text content
- <xsl:text> // with jumps at first and last entry when appropriate
- <xsl:text> set_partial_text: function(){
- <xsl:text> let spans = this.text_elt.children;
- <xsl:text> let contentlength = this.content.length;
- <xsl:text> let spanslength = spans.length;
- <xsl:text> let i = this.menu_offset, c = 0;
- <xsl:text> while(c < spanslength){
- <xsl:text> let span=spans[c];
- <xsl:text> // backward jump only present if not exactly at start
- <xsl:text> if(c == 0 && i != 0){
- <xsl:text> span.textContent = "↑ ↑ ↑";
- <xsl:text> span.setAttribute("onclick", "hmi_widgets['</xsl:text>
- <xsl:value-of select="$hmi_element/@id"/>
- <xsl:text>'].on_backward_click()");
- <xsl:text> // presence of forward jump when not right at the end
- <xsl:text> }else if(c == spanslength-1 && i < contentlength - 1){
- <xsl:text> span.textContent = "↓ ↓ ↓";
- <xsl:text> span.setAttribute("onclick", "hmi_widgets['</xsl:text>
- <xsl:value-of select="$hmi_element/@id"/>
- <xsl:text>'].on_forward_click()");
- <xsl:text> // otherwise normal content
- <xsl:text> span.textContent = this.content[i];
- <xsl:text> span.setAttribute("onclick", "hmi_widgets['</xsl:text>
- <xsl:value-of select="$hmi_element/@id"/>
- <xsl:text>'].on_selection_click("+i+")");
- <xsl:text> open: function(){
- <xsl:text> let length = this.content.length;
- <xsl:text> // systematically reset text, to strip eventual whitespace spans
- <xsl:text> this.reset_text();
- <xsl:text> // grow as much as needed or possible
- <xsl:text> let slots = this.grow_text(length);
- <xsl:text> // Depending on final size
- <xsl:text> if(slots == length) {
- <xsl:text> // show all at once
- <xsl:text> this.set_complete_text();
- <xsl:text> // eventualy align menu to current selection, compensating for lift
- <xsl:text> let offset = this.last_selection - this.lift;
- <xsl:text> if(offset > 0)
- <xsl:text> this.menu_offset = Math.min(offset + 1, length - slots + 1);
- <xsl:text> this.menu_offset = 0;
- <xsl:text> // show surrounding values
- <xsl:text> this.set_partial_text();
- <xsl:text> // Now that text size is known, we can set the box around it
- <xsl:text> this.adjust_box_to_text();
- <xsl:text> // Take button out until menu closed
- <xsl:text> this.element.removeChild(this.button_elt);
- <xsl:text> // Rise widget to top by moving it to last position among siblings
- <xsl:text> this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element));
- <xsl:text> // disable interaction with background
- <xsl:text> svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true);
- <xsl:text> // mark as open
- <xsl:text> this.opened = true;
- <xsl:text> // Put text element in normalized state
- <xsl:text> reset_text: function(){
- <xsl:text> let txt = this.text_elt;
- <xsl:text> let first = txt.firstElementChild;
- <xsl:text> // remove attribute eventually added to first text line while opening
- <xsl:text> first.removeAttribute("onclick");
- <xsl:text> first.removeAttribute("dy");
- <xsl:text> // keep only the first line of text
- <xsl:text> for(let span of Array.from(txt.children).slice(1)){
- <xsl:text> txt.removeChild(span)
- <xsl:text> // Put rectangle element in saved original state
- <xsl:text> reset_box: function(){
- <xsl:text> let m = this.box_bbox;
- <xsl:text> let b = this.box_elt;
- <xsl:text> b.x.baseVal.value = m.x;
- <xsl:text> b.y.baseVal.value = m.y;
- <xsl:text> b.width.baseVal.value = m.width;
- <xsl:text> b.height.baseVal.value = m.height;
- <xsl:text> // Use margin and text size to compute box size
- <xsl:text> adjust_box_to_text: function(){
- <xsl:text> let [lmargin, tmargin] = this.margins;
- <xsl:text> let m = this.text_elt.getBBox();
- <xsl:text> let b = this.box_elt;
- <xsl:text> b.x.baseVal.value = m.x - lmargin;
- <xsl:text> b.y.baseVal.value = m.y - tmargin;
- <xsl:text> b.width.baseVal.value = 2 * lmargin + m.width;
- <xsl:text> b.height.baseVal.value = 2 * tmargin + m.height;
<xsl:template mode="widget_defs" match="widget[@type='ForEach']">
--- a/svghmi/widget_dropdown.ysl2 Thu Dec 10 15:48:15 2020 +0100
+++ b/svghmi/widget_dropdown.ysl2 Tue Dec 15 13:43:21 2020 +0100
@@ -1,252 +1,262 @@
+template "widget[@type='DropDown']", mode="widget_class"{ + class DropDownWidget extends Widget{ + if(!this.opened) this.set_selection(value); + this.button_elt.onclick = this.on_button_click.bind(this); + // Save original size of rectangle + this.box_bbox = this.box_elt.getBBox() + let text_bbox = this.text_elt.getBBox(); + let lmargin = text_bbox.x - this.box_bbox.x; + let tmargin = text_bbox.y - this.box_bbox.y; + this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); + // Index of first visible element in the menu, when opened + // How mutch to lift the menu vertically so that it does not cross bottom border + // Event handlers cannot be object method ('this' is unknown) + // as a workaround, handler given to addEventListener is bound in advance. + this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this); + this.bound_on_selection_click = this.on_selection_click.bind(this); + this.bound_on_backward_click = this.on_backward_click.bind(this); + this.bound_on_forward_click = this.on_forward_click.bind(this); + // Called when a menu entry is clicked + on_selection_click(selection) { + this.apply_hmi_value(0, selection); + if(value >= 0 && value < this.content.length){ + // if valid selection resolve content + display_str = this.content[value]; + this.last_selection = value; + // otherwise show problem + display_str = "?"+String(value)+"?"; + // It is assumed that first span always stays, + // and contains selection when menu is closed + this.text_elt.firstElementChild.textContent = display_str; + let txt = this.text_elt; + let first = txt.firstElementChild; + // Real world (pixels) boundaries of current page + let bounds = svg_root.getBoundingClientRect(); + let next = first.cloneNode(); + // relative line by line text flow instead of absolute y coordinate + next.removeAttribute("y"); + next.setAttribute("dy", "1.1em"); + // default content to allow computing text element bbox + next.textContent = "..."; + // append new span to text element + // now check if text extended by one row fits to page + // FIXME : exclude margins to be more accurate on box size + let rect = txt.getBoundingClientRect(); + if(rect.bottom > bounds.bottom){ + // in case of overflow at the bottom, lift up one row + let backup = first.getAttribute("dy"); + // apply lift asr a dy added too first span (y attrib stays) + first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em"); + rect = txt.getBoundingClientRect(); + if(rect.top > bounds.top){ + // if it goes over the top, then backtrack + // restore dy attribute on first span + first.setAttribute("dy", backup); + first.removeAttribute("dy"); + // remove unwanted child + close_on_click_elsewhere(e) { + // inhibit events not targetting spans (menu items) + if(e.target.parentNode !== this.text_elt){ + // close menu in case click is outside box + if(e.target !== this.box_elt) + // Stop hogging all click events + svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); + // Restore position and sixe of widget elements + // Put the button back in place + this.element.appendChild(this.button_elt); + // Mark as closed (to allow dispatch) + // Dispatch last cached value + // Set text content when content is smaller than menu (no scrolling) + let spans = this.text_elt.children; + for(let item of this.content){ + span.textContent = item; + span.onclick = (evt) => this.bound_on_selection_click(sel); + // false : upward, lower value + // true : downward, higher value + let contentlength = this.content.length; + let spans = this.text_elt.children; + let spanslength = spans.length; + // reduce accounted menu size according to jumps + if(this.menu_offset != 0) spanslength--; + if(this.menu_offset < contentlength - 1) spanslength--; + this.menu_offset = Math.min( + contentlength - spans.length + 1, + this.menu_offset + spanslength); + this.menu_offset = Math.max( + this.menu_offset - spanslength); + this.set_partial_text(); + // Setup partial view text content + // with jumps at first and last entry when appropriate + let spans = this.text_elt.children; + let contentlength = this.content.length; + let spanslength = spans.length; + let i = this.menu_offset, c = 0; + while(c < spanslength){ + // backward jump only present if not exactly at start + span.textContent = "↑ ↑ ↑"; + span.onclick = this.bound_on_backward_click; + // presence of forward jump when not right at the end + }else if(c == spanslength-1 && i < contentlength - 1){ + span.textContent = "↓ ↓ ↓"; + span.onclick = this.bound_on_forward_click; + // otherwise normal content + span.textContent = this.content[i]; + span.onclick = (evt) => this.bound_on_selection_click(sel); + let length = this.content.length; + // systematically reset text, to strip eventual whitespace spans + // grow as much as needed or possible + let slots = this.grow_text(length); + // Depending on final size + this.set_complete_text(); + // eventualy align menu to current selection, compensating for lift + let offset = this.last_selection - this.lift; + this.menu_offset = Math.min(offset + 1, length - slots + 1); + // show surrounding values + this.set_partial_text(); + // Now that text size is known, we can set the box around it + this.adjust_box_to_text(); + // Take button out until menu closed + this.element.removeChild(this.button_elt); + // Rise widget to top by moving it to last position among siblings + this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); + // disable interaction with background + svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); + // Put text element in normalized state + let txt = this.text_elt; + let first = txt.firstElementChild; + // remove attribute eventually added to first text line while opening + first.removeAttribute("onclick"); + first.removeAttribute("dy"); + // keep only the first line of text + for(let span of Array.from(txt.children).slice(1)){ + // Put rectangle element in saved original state + b.x.baseVal.value = m.x; + b.y.baseVal.value = m.y; + b.width.baseVal.value = m.width; + b.height.baseVal.value = m.height; + // Use margin and text size to compute box size + let [lmargin, tmargin] = this.margins; + let m = this.text_elt.getBBox(); + b.x.baseVal.value = m.x - lmargin; + b.y.baseVal.value = m.y - tmargin; + b.width.baseVal.value = 2 * lmargin + m.width; + b.height.baseVal.value = 2 * tmargin + m.height; template "widget[@type='DropDown']", mode="widget_defs" {
labels("text box button");
- dispatch: function(value) {
- if(!this.opened) this.set_selection(value);
- this.button_elt.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_button_click()");
- // Save original size of rectangle
- this.box_bbox = this.box_elt.getBBox()
- text_bbox = this.text_elt.getBBox()
- lmargin = text_bbox.x - this.box_bbox.x;
- tmargin = text_bbox.y - this.box_bbox.y;
- this.margins = [lmargin, tmargin].map(x => Math.max(x,0));
- // It is assumed that list content conforms to Array interface.
- ``foreach "arg" | "«@value»",
- // Index of first visible element in the menu, when opened
- // How mutch to lift the menu vertically so that it does not cross bottom border
- // Event handlers cannot be object method ('this' is unknown)
- // as a workaround, handler given to addEventListener is bound in advance.
- this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this);
+ // It is assumed that list content conforms to Array interface. + ``foreach "arg" | "«@value»",
- // Called when a menu entry is clicked
- on_selection_click: function(selection) {
- this.apply_hmi_value(0, selection);
- on_button_click: function() {
- on_backward_click: function(){
- on_forward_click:function(){
- set_selection: function(value) {
- if(value >= 0 && value < this.content.length){
- // if valid selection resolve content
- display_str = this.content[value];
- this.last_selection = value;
- // otherwise show problem
- display_str = "?"+String(value)+"?";
- // It is assumed that first span always stays,
- // and contains selection when menu is closed
- this.text_elt.firstElementChild.textContent = display_str;
- grow_text: function(up_to) {
- let txt = this.text_elt;
- let first = txt.firstElementChild;
- // Real world (pixels) boundaries of current page
- let bounds = svg_root.getBoundingClientRect();
- let next = first.cloneNode();
- // relative line by line text flow instead of absolute y coordinate
- next.removeAttribute("y");
- next.setAttribute("dy", "1.1em");
- // default content to allow computing text element bbox
- next.textContent = "...";
- // append new span to text element
- // now check if text extended by one row fits to page
- // FIXME : exclude margins to be more accurate on box size
- let rect = txt.getBoundingClientRect();
- if(rect.bottom > bounds.bottom){
- // in case of overflow at the bottom, lift up one row
- let backup = first.getAttribute("dy");
- // apply lift asr a dy added too first span (y attrib stays)
- first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em");
- rect = txt.getBoundingClientRect();
- if(rect.top > bounds.top){
- // if it goes over the top, then backtrack
- // restore dy attribute on first span
- first.setAttribute("dy", backup);
- first.removeAttribute("dy");
- // remove unwanted child
- close_on_click_elsewhere: function(e) {
- // inhibit events not targetting spans (menu items)
- if(e.target.parentNode !== this.text_elt){
- // close menu in case click is outside box
- if(e.target !== this.box_elt)
- // Stop hogging all click events
- svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true);
- // Restore position and sixe of widget elements
- // Put the button back in place
- this.element.appendChild(this.button_elt);
- // Mark as closed (to allow dispatch)
- // Dispatch last cached value
- // Set text content when content is smaller than menu (no scrolling)
- set_complete_text: function(){
- let spans = this.text_elt.children;
- for(let item of this.content){
- span.textContent = item;
- span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_selection_click("+c+")");
- // false : upward, lower value
- // true : downward, higher value
- scroll: function(forward){
- let contentlength = this.content.length;
- let spans = this.text_elt.children;
- let spanslength = spans.length;
- // reduce accounted menu size according to jumps
- if(this.menu_offset != 0) spanslength--;
- if(this.menu_offset < contentlength - 1) spanslength--;
- this.menu_offset = Math.min(
- contentlength - spans.length + 1,
- this.menu_offset + spanslength);
- this.menu_offset = Math.max(
- this.menu_offset - spanslength);
- this.set_partial_text();
- // Setup partial view text content
- // with jumps at first and last entry when appropriate
- set_partial_text: function(){
- let spans = this.text_elt.children;
- let contentlength = this.content.length;
- let spanslength = spans.length;
- let i = this.menu_offset, c = 0;
- while(c < spanslength){
- // backward jump only present if not exactly at start
- span.textContent = "↑ ↑ ↑";
- span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_backward_click()");
- // presence of forward jump when not right at the end
- }else if(c == spanslength-1 && i < contentlength - 1){
- span.textContent = "↓ ↓ ↓";
- span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_forward_click()");
- // otherwise normal content
- span.textContent = this.content[i];
- span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_selection_click("+i+")");
- let length = this.content.length;
- // systematically reset text, to strip eventual whitespace spans
- // grow as much as needed or possible
- let slots = this.grow_text(length);
- // Depending on final size
- this.set_complete_text();
- // eventualy align menu to current selection, compensating for lift
- let offset = this.last_selection - this.lift;
- this.menu_offset = Math.min(offset + 1, length - slots + 1);
- // show surrounding values
- this.set_partial_text();
- // Now that text size is known, we can set the box around it
- this.adjust_box_to_text();
- // Take button out until menu closed
- this.element.removeChild(this.button_elt);
- // Rise widget to top by moving it to last position among siblings
- this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element));
- // disable interaction with background
- svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true);
- // Put text element in normalized state
- reset_text: function(){
- let txt = this.text_elt;
- let first = txt.firstElementChild;
- // remove attribute eventually added to first text line while opening
- first.removeAttribute("onclick");
- first.removeAttribute("dy");
- // keep only the first line of text
- for(let span of Array.from(txt.children).slice(1)){
- // Put rectangle element in saved original state
- b.x.baseVal.value = m.x;
- b.y.baseVal.value = m.y;
- b.width.baseVal.value = m.width;
- b.height.baseVal.value = m.height;
- // Use margin and text size to compute box size
- adjust_box_to_text: function(){
- let [lmargin, tmargin] = this.margins;
- let m = this.text_elt.getBBox();
- b.x.baseVal.value = m.x - lmargin;
- b.y.baseVal.value = m.y - tmargin;
- b.width.baseVal.value = 2 * lmargin + m.width;
- b.height.baseVal.value = 2 * tmargin + m.height;