# HG changeset patch # User Edouard Tisserant # Date 1608229860 -3600 # Node ID 96ffd8b1b0169f6b0c763cb4281c226859a754d0 # Parent f475f39713aa94007f688a5f753be40262aac5d1 SVGHMI: added dropdown selection highlighting and fixed scrolling so that it doesn't miss any entry while jumping from one page to the other. diff -r f475f39713aa -r 96ffd8b1b016 svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Wed Dec 16 15:44:24 2020 +0100 +++ b/svghmi/gen_index_xhtml.xslt Thu Dec 17 19:31:00 2020 +0100 @@ -3084,6 +3084,10 @@ this.box_bbox = this.box_elt.getBBox() + this.highlight_bbox = this.highlight_elt.getBBox() + + this.highlight_elt.style.visibility = "hidden"; + // Compute margins @@ -3228,7 +3232,7 @@ let backup = first.getAttribute("dy"); - // apply lift asr a dy added too first span (y attrib stays) + // apply lift as a dy added too first span (y attrib stays) first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em"); @@ -3306,6 +3310,8 @@ this.reset_box(); + this.reset_highlight(); + // Put the button back in place this.element.appendChild(this.button_elt); @@ -3326,289 +3332,359 @@ let txt = this.text_elt; + let original_text_y = this.text_bbox.y; + + let highlight = this.highlight_elt; + + let original_h_y = this.highlight_bbox.y; + + let clickable = highlight.cloneNode(); + + let yoffset = span.getBBox().y - original_text_y; + + clickable.y.baseVal.value = original_h_y + yoffset; + + clickable.style.pointerEvents = "bounding-box"; + + //clickable.style.visibility = "hidden"; + + //clickable.onclick = () => alert("love JS"); + + clickable.onclick = func; + + this.element.appendChild(clickable); + + this.clickables.push(clickable) + + } + + reset_clickables() { + + while(this.clickables.length){ + + this.element.removeChild(this.clickables.pop()); + + } + + } + + // 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; + + this.make_clickable(span, (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 prsence of scroll buttons + + // since we scroll there is necessarly one button + + spanslength--; + + if(forward){ + + // reduce accounted menu size because of back button + + if(this.menu_offset != 0) spanslength--; + + this.menu_offset = Math.min( + + contentlength - spans.length + 1, + + this.menu_offset + spanslength); + + }else{ + + if(this.menu_offset - spanslength > 0) spanslength--; + + this.menu_offset = Math.max( + + 0, + + this.menu_offset - spanslength); + + } + + if(this.menu_offset == 1) + + this.menu_offset = 0; + + + + this.reset_highlight(); + + + + this.reset_clickables(); + + this.set_partial_text(); + + + + this.highlight_selection(); + + } + + // 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; + + let m = this.box_bbox; + + while(c < spanslength){ + + let span=spans[c]; + + let onclickfunc; + + // backward jump only present if not exactly at start + + if(c == 0 && i != 0){ + + span.textContent = "▲"; + + onclickfunc = this.bound_on_backward_click; + + let o = span.getBBox(); + + span.setAttribute("dx", (m.width - o.width)/2); + + // presence of forward jump when not right at the end + + }else if(c == spanslength-1 && i < contentlength - 1){ + + span.textContent = "▼"; + + onclickfunc = this.bound_on_forward_click; + + let o = span.getBBox(); + + span.setAttribute("dx", (m.width - o.width)/2); + + // otherwise normal content + + }else{ + + span.textContent = this.content[i]; + + let sel = i; + + onclickfunc = (evt) => this.bound_on_selection_click(sel); + + span.removeAttribute("dx"); + + i++; + + } + + this.make_clickable(span, onclickfunc); + + 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("pointerdown", numb_event, true); + + svg_root.addEventListener("pointerup", numb_event, true); + + svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); + + this.highlight_selection(); + + + + // 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.onclick = null; + + first.removeAttribute("dy"); + + first.removeAttribute("dx"); + + // 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; + + } + + highlight_selection(){ + + let highlighted_row = this.last_selection - this.menu_offset; + + if(highlighted_row < 0) return; + + let spans = this.text_elt.children; + + let spanslength = spans.length; + + let contentlength = this.content.length; + + if(this.menu_offset != 0) { + + spanslength--; + + highlighted_row++; + + } + + if(this.menu_offset + spanslength < contentlength - 1) spanslength--; + + if(highlighted_row > spanslength) return; + let original_text_y = this.text_bbox.y; let highlight = this.highlight_elt; - let original_h_y = highlight.getBBox().y; - - let clickable = highlight.cloneNode(); + let span = spans[highlighted_row]; let yoffset = span.getBBox().y - original_text_y; - clickable.setAttribute("y", original_h_y + yoffset); - - clickable.style.pointerEvents = "bounding-box"; - - clickable.style.visibility = "hidden"; - - //clickable.onclick = () => alert("love JS"); - - clickable.onclick = func; - - this.element.appendChild(clickable); - - this.clickables.push(clickable) - - } - - reset_clickables() { - - while(this.clickables.length){ - - this.element.removeChild(this.clickables.pop()); - - } - - } - - // 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; - - this.make_clickable(span, (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.reset_clickables(); - - 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; - - let m = this.box_bbox; - - while(c < spanslength){ - - let span=spans[c]; - - let onclickfunc; - - // backward jump only present if not exactly at start - - if(c == 0 && i != 0){ - - span.textContent = "▲"; - - onclickfunc = this.bound_on_backward_click; - - let o = span.getBBox(); - - span.setAttribute("dx", (m.width - o.width)/2); - - // presence of forward jump when not right at the end - - }else if(c == spanslength-1 && i < contentlength - 1){ - - span.textContent = "▼"; - - onclickfunc = this.bound_on_forward_click; - - let o = span.getBBox(); - - span.setAttribute("dx", (m.width - o.width)/2); - - // otherwise normal content - - }else{ - - span.textContent = this.content[i]; - - let sel = i; - - onclickfunc = (evt) => this.bound_on_selection_click(sel); - - span.removeAttribute("dx"); - - i++; - - } - - this.make_clickable(span, onclickfunc); - - 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("pointerdown", numb_event, true); - - svg_root.addEventListener("pointerup", numb_event, true); - - 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.onclick = null; - - first.removeAttribute("dy"); - - first.removeAttribute("dx"); - - // 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; + highlight.y.baseVal.value = this.highlight_bbox.y + yoffset; + + highlight.style.visibility = "visible"; + + } + + reset_highlight(){ + + let highlight = this.highlight_elt; + + highlight.y.baseVal.value = this.highlight_bbox.y; + + highlight.style.visibility = "hidden"; } diff -r f475f39713aa -r 96ffd8b1b016 svghmi/widget_dropdown.ysl2 --- a/svghmi/widget_dropdown.ysl2 Wed Dec 16 15:44:24 2020 +0100 +++ b/svghmi/widget_dropdown.ysl2 Thu Dec 17 19:31:00 2020 +0100 @@ -13,6 +13,8 @@ this.button_elt.onclick = this.on_button_click.bind(this); // Save original size of rectangle this.box_bbox = this.box_elt.getBBox() + this.highlight_bbox = this.highlight_elt.getBBox() + this.highlight_elt.style.visibility = "hidden"; // Compute margins this.text_bbox = this.text_elt.getBBox(); @@ -85,7 +87,7 @@ 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) + // apply lift as 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){ @@ -124,6 +126,7 @@ this.reset_text(); this.reset_clickables(); this.reset_box(); + this.reset_highlight(); // Put the button back in place this.element.appendChild(this.button_elt); // Mark as closed (to allow dispatch) @@ -134,15 +137,14 @@ // Make item (text span) clickable by overlaying a rectangle on top of it make_clickable(span, func) { let txt = this.text_elt; - let first = txt.firstElementChild; let original_text_y = this.text_bbox.y; let highlight = this.highlight_elt; - let original_h_y = highlight.getBBox().y; + let original_h_y = this.highlight_bbox.y; let clickable = highlight.cloneNode(); let yoffset = span.getBBox().y - original_text_y; - clickable.setAttribute("y", original_h_y + yoffset); + clickable.y.baseVal.value = original_h_y + yoffset; clickable.style.pointerEvents = "bounding-box"; - clickable.style.visibility = "hidden"; + //clickable.style.visibility = "hidden"; //clickable.onclick = () => alert("love JS"); clickable.onclick = func; this.element.appendChild(clickable); @@ -172,20 +174,33 @@ 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--; + // reduce accounted menu size according to prsence of scroll buttons + // since we scroll there is necessarly one button + spanslength--; if(forward){ + // reduce accounted menu size because of back button + // in current view + if(this.menu_offset > 0) spanslength--; this.menu_offset = Math.min( contentlength - spans.length + 1, this.menu_offset + spanslength); }else{ + // reduce accounted menu size because of back button + // in view once scrolled + if(this.menu_offset - spanslength > 0) spanslength--; this.menu_offset = Math.max( 0, this.menu_offset - spanslength); } + if(this.menu_offset == 1) + this.menu_offset = 0; + + this.reset_highlight(); + this.reset_clickables(); this.set_partial_text(); + + this.highlight_selection(); } // Setup partial view text content // with jumps at first and last entry when appropriate @@ -252,6 +267,8 @@ svg_root.addEventListener("pointerdown", numb_event, true); svg_root.addEventListener("pointerup", numb_event, true); svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); + this.highlight_selection(); + // mark as open this.opened = true; } @@ -277,6 +294,30 @@ b.width.baseVal.value = m.width; b.height.baseVal.value = m.height; } + highlight_selection(){ + let highlighted_row = this.last_selection - this.menu_offset; + if(highlighted_row < 0) return; + let spans = this.text_elt.children; + let spanslength = spans.length; + let contentlength = this.content.length; + if(this.menu_offset != 0) { + spanslength--; + highlighted_row++; + } + if(this.menu_offset + spanslength < contentlength - 1) spanslength--; + if(highlighted_row > spanslength) return; + let original_text_y = this.text_bbox.y; + let highlight = this.highlight_elt; + let span = spans[highlighted_row]; + let yoffset = span.getBBox().y - original_text_y; + highlight.y.baseVal.value = this.highlight_bbox.y + yoffset; + highlight.style.visibility = "visible"; + } + reset_highlight(){ + let highlight = this.highlight_elt; + highlight.y.baseVal.value = this.highlight_bbox.y; + highlight.style.visibility = "hidden"; + } // Use margin and text size to compute box size adjust_box_to_text(){ let [lmargin, tmargin] = this.margins; diff -r f475f39713aa -r 96ffd8b1b016 tests/svghmi/svghmi_0@svghmi/svghmi.svg --- a/tests/svghmi/svghmi_0@svghmi/svghmi.svg Wed Dec 16 15:44:24 2020 +0100 +++ b/tests/svghmi/svghmi_0@svghmi/svghmi.svg Thu Dec 17 19:31:00 2020 +0100 @@ -197,12 +197,12 @@ inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:document-units="px" - inkscape:current-layer="g14237" + inkscape:current-layer="hmi0" showgrid="false" units="px" inkscape:zoom="0.54565796" - inkscape:cx="147.6698" - inkscape:cy="180.09341" + inkscape:cx="-623.8758" + inkscape:cy="172.76281" inkscape:window-width="1600" inkscape:window-height="836" inkscape:window-x="0" @@ -2394,7 +2394,7 @@