# HG changeset patch # User Edouard Tisserant # Date 1608129864 -3600 # Node ID f475f39713aa94007f688a5f753be40262aac5d1 # Parent 9e172e4e50c7c676eaa8033c7bdbe4c0bb32a4c6 SVGHMI: change scroll buttons into single unicode triangle and center them. Use highlight rectangle duplicated and hidden to catch selection clicks so that the whole row is clickable. diff -r 9e172e4e50c7 -r f475f39713aa svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Tue Dec 15 13:43:21 2020 +0100 +++ b/svghmi/gen_index_xhtml.xslt Wed Dec 16 15:44:24 2020 +0100 @@ -3062,6 +3062,12 @@ + function numb_event(e) { + + e.stopPropagation(); + + } + class DropDownWidget extends Widget{ dispatch(value) { @@ -3082,11 +3088,11 @@ // 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.text_bbox = this.text_elt.getBBox(); + + let lmargin = this.text_bbox.x - this.box_bbox.x; + + let tmargin = this.text_bbox.y - this.box_bbox.y; this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); @@ -3118,6 +3124,8 @@ this.opened = false; + this.clickables = []; + } on_button_click() { @@ -3180,13 +3188,13 @@ let count = 1; - let txt = this.text_elt; + let txt = this.text_elt; let first = txt.firstElementChild; // Real world (pixels) boundaries of current page - let bounds = svg_root.getBoundingClientRect(); + let bounds = svg_root.getBoundingClientRect(); this.lift = 0; @@ -3266,7 +3274,7 @@ // inhibit events not targetting spans (menu items) - if(e.target.parentNode !== this.text_elt){ + if([this.text_elt, this.element].indexOf(e.target.parentNode) == -1){ e.stopPropagation(); @@ -3284,12 +3292,18 @@ // Stop hogging all click events + svg_root.removeEventListener("pointerdown", numb_event, true); + + svg_root.removeEventListener("pointerup", numb_event, true); + svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); // Restore position and sixe of widget elements this.reset_text(); + this.reset_clickables(); + this.reset_box(); // Put the button back in place @@ -3306,11 +3320,55 @@ } + // 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 clickable = highlight.cloneNode(); + + 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 spans = this.text_elt.children; let c = 0; @@ -3320,7 +3378,9 @@ span.textContent = item; - span.onclick = (evt) => this.bound_on_selection_click(c); + let sel = c; + + this.make_clickable(span, (evt) => this.bound_on_selection_click(sel)); c++; @@ -3338,7 +3398,7 @@ let contentlength = this.content.length; - let spans = this.text_elt.children; + let spans = this.text_elt.children; let spanslength = spans.length; @@ -3352,7 +3412,7 @@ this.menu_offset = Math.min( - contentlength - spans.length + 1, + contentlength - spans.length + 1, this.menu_offset + spanslength); @@ -3360,12 +3420,14 @@ this.menu_offset = Math.max( - 0, + 0, this.menu_offset - spanslength); } + this.reset_clickables(); + this.set_partial_text(); } @@ -3376,7 +3438,7 @@ set_partial_text(){ - let spans = this.text_elt.children; + let spans = this.text_elt.children; let contentlength = this.content.length; @@ -3384,25 +3446,37 @@ 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 = "↑ ↑ ↑"; - - span.onclick = this.bound_on_backward_click; + 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 = "↓ ↓ ↓"; - - span.onclick = this.bound_on_forward_click; + span.textContent = "▼"; + + onclickfunc = this.bound_on_forward_click; + + let o = span.getBBox(); + + span.setAttribute("dx", (m.width - o.width)/2); // otherwise normal content @@ -3412,12 +3486,16 @@ let sel = i; - span.onclick = (evt) => this.bound_on_selection_click(sel); + onclickfunc = (evt) => this.bound_on_selection_click(sel); + + span.removeAttribute("dx"); i++; } + this.make_clickable(span, onclickfunc); + c++; } @@ -3478,6 +3556,10 @@ // 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 @@ -3490,16 +3572,18 @@ reset_text(){ - let txt = this.text_elt; + let txt = this.text_elt; let first = txt.firstElementChild; // remove attribute eventually added to first text line while opening - first.removeAttribute("onclick"); + 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)){ @@ -3538,11 +3622,11 @@ let b = this.box_elt; - b.x.baseVal.value = m.x - lmargin; + // b.x.baseVal.value = m.x - lmargin; b.y.baseVal.value = m.y - tmargin; - b.width.baseVal.value = 2 * lmargin + m.width; + // b.width.baseVal.value = 2 * lmargin + m.width; b.height.baseVal.value = 2 * tmargin + m.height; @@ -3556,7 +3640,7 @@ - text box button + text box button highlight // It is assumed that list content conforms to Array interface. diff -r 9e172e4e50c7 -r f475f39713aa svghmi/widget_dropdown.ysl2 --- a/svghmi/widget_dropdown.ysl2 Tue Dec 15 13:43:21 2020 +0100 +++ b/svghmi/widget_dropdown.ysl2 Wed Dec 16 15:44:24 2020 +0100 @@ -2,6 +2,9 @@ template "widget[@type='DropDown']", mode="widget_class"{ || + function numb_event(e) { + e.stopPropagation(); + } class DropDownWidget extends Widget{ dispatch(value) { if(!this.opened) this.set_selection(value); @@ -12,9 +15,9 @@ 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.text_bbox = this.text_elt.getBBox(); + let lmargin = this.text_bbox.x - this.box_bbox.x; + let tmargin = this.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 @@ -30,6 +33,7 @@ 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; + this.clickables = []; } on_button_click() { this.open(); @@ -61,10 +65,10 @@ } grow_text(up_to) { let count = 1; - let txt = this.text_elt; + let txt = this.text_elt; let first = txt.firstElementChild; // Real world (pixels) boundaries of current page - let bounds = svg_root.getBoundingClientRect(); + let bounds = svg_root.getBoundingClientRect(); this.lift = 0; while(count < up_to) { let next = first.cloneNode(); @@ -104,7 +108,7 @@ } close_on_click_elsewhere(e) { // inhibit events not targetting spans (menu items) - if(e.target.parentNode !== this.text_elt){ + if([this.text_elt, this.element].indexOf(e.target.parentNode) == -1){ e.stopPropagation(); // close menu in case click is outside box if(e.target !== this.box_elt) @@ -113,9 +117,12 @@ } close(){ // Stop hogging all click events + svg_root.removeEventListener("pointerdown", numb_event, true); + svg_root.removeEventListener("pointerup", numb_event, true); svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); // Restore position and sixe of widget elements this.reset_text(); + this.reset_clickables(); this.reset_box(); // Put the button back in place this.element.appendChild(this.button_elt); @@ -124,15 +131,37 @@ // Dispatch last cached value this.apply_cache(); } + // 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 clickable = highlight.cloneNode(); + 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 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); + this.make_clickable(span, (evt) => this.bound_on_selection_click(sel)); c++; } } @@ -141,46 +170,55 @@ // true : downward, higher value scroll(forward){ let contentlength = this.content.length; - let spans = this.text_elt.children; + 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, + contentlength - spans.length + 1, this.menu_offset + spanslength); }else{ this.menu_offset = Math.max( - 0, + 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 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 = "↑ ↑ ↑"; - span.onclick = this.bound_on_backward_click; + 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 = "↓ ↓ ↓"; - span.onclick = this.bound_on_forward_click; + 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; - span.onclick = (evt) => this.bound_on_selection_click(sel); + onclickfunc = (evt) => this.bound_on_selection_click(sel); + span.removeAttribute("dx"); i++; } + this.make_clickable(span, onclickfunc); c++; } } @@ -211,17 +249,20 @@ // 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 txt = this.text_elt; let first = txt.firstElementChild; // remove attribute eventually added to first text line while opening - first.removeAttribute("onclick"); + 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) @@ -241,17 +282,18 @@ let [lmargin, tmargin] = this.margins; let m = this.text_elt.getBBox(); let b = this.box_elt; - b.x.baseVal.value = m.x - lmargin; + // b.x.baseVal.value = m.x - lmargin; b.y.baseVal.value = m.y - tmargin; - b.width.baseVal.value = 2 * lmargin + m.width; + // 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"); + labels("text box button highlight"); || // It is assumed that list content conforms to Array interface. content: [ diff -r 9e172e4e50c7 -r f475f39713aa tests/svghmi/svghmi_0@svghmi/svghmi.svg --- a/tests/svghmi/svghmi_0@svghmi/svghmi.svg Tue Dec 15 13:43:21 2020 +0100 +++ b/tests/svghmi/svghmi_0@svghmi/svghmi.svg Wed Dec 16 15:44:24 2020 +0100 @@ -197,19 +197,20 @@ inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:document-units="px" - inkscape:current-layer="g443" + inkscape:current-layer="g14237" showgrid="false" units="px" - inkscape:zoom="0.77167689" - inkscape:cx="379.87087" - inkscape:cy="462.91635" - inkscape:window-width="1848" - inkscape:window-height="1016" - inkscape:window-x="72" + inkscape:zoom="0.54565796" + inkscape:cx="147.6698" + inkscape:cy="180.09341" + inkscape:window-width="1600" + inkscape:window-height="836" + inkscape:window-x="0" inkscape:window-y="27" inkscape:window-maximized="1" showguides="true" - inkscape:guide-bbox="true" /> + inkscape:guide-bbox="true" + inkscape:snap-global="false" /> + + transform="matrix(0.28590269,0,0,0.28590269,85.246911,560.98603)"> @@ -2542,7 +2553,7 @@ height="95.723877" width="245.44583" id="rect433" - style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#fdfdfd;fill-opacity:1;fill-rule:nonzero;stroke:#ffd0b2;stroke-width:28.60938356;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#fdfdfd;fill-opacity:1;fill-rule:nonzero;stroke:#ffd0b2;stroke-width:28.60938263;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />