# 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 @@ - - class CircularBarWidget extends Widget{ @@ -3063,6 +3061,496 @@ + + 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; + + span.onclick = (evt) => this.bound_on_selection_click(c); + + 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; + + } + + } + + @@ -3071,39 +3559,9 @@ text box button - dispatch: function(value) { - - if(!this.opened) this.set_selection(value); - - }, - - init: function() { - - this.button_elt.setAttribute("onclick", "hmi_widgets[' - - '].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 = [ + // It is assumed that list content conforms to Array interface. + + content: [ " @@ -3111,467 +3569,9 @@ ", - ]; - - - - // 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[' - - '].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[' - - '].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[' - - '].on_forward_click()"); - - // otherwise normal content - - }else{ - - span.textContent = this.content[i]; - - span.setAttribute("onclick", "hmi_widgets[' - - '].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; - - }, + ], + + 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»", + ], + || }