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