1 // widget_dropdown.ysl2 |
1 // widget_dropdown.ysl2 |
2 |
2 |
3 template "widget[@type='DropDown']", mode="widget_class"{ |
3 template "widget[@type='DropDown']", mode="widget_class"{ |
4 || |
4 || |
|
5 function numb_event(e) { |
|
6 e.stopPropagation(); |
|
7 } |
5 class DropDownWidget extends Widget{ |
8 class DropDownWidget extends Widget{ |
6 dispatch(value) { |
9 dispatch(value) { |
7 if(!this.opened) this.set_selection(value); |
10 if(!this.opened) this.set_selection(value); |
8 } |
11 } |
9 init() { |
12 init() { |
10 this.button_elt.onclick = this.on_button_click.bind(this); |
13 this.button_elt.onclick = this.on_button_click.bind(this); |
11 // Save original size of rectangle |
14 // Save original size of rectangle |
12 this.box_bbox = this.box_elt.getBBox() |
15 this.box_bbox = this.box_elt.getBBox() |
13 |
16 |
14 // Compute margins |
17 // Compute margins |
15 let text_bbox = this.text_elt.getBBox(); |
18 this.text_bbox = this.text_elt.getBBox(); |
16 let lmargin = text_bbox.x - this.box_bbox.x; |
19 let lmargin = this.text_bbox.x - this.box_bbox.x; |
17 let tmargin = text_bbox.y - this.box_bbox.y; |
20 let tmargin = this.text_bbox.y - this.box_bbox.y; |
18 this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); |
21 this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); |
19 |
22 |
20 // Index of first visible element in the menu, when opened |
23 // Index of first visible element in the menu, when opened |
21 this.menu_offset = 0; |
24 this.menu_offset = 0; |
22 |
25 |
102 } |
106 } |
103 return count; |
107 return count; |
104 } |
108 } |
105 close_on_click_elsewhere(e) { |
109 close_on_click_elsewhere(e) { |
106 // inhibit events not targetting spans (menu items) |
110 // inhibit events not targetting spans (menu items) |
107 if(e.target.parentNode !== this.text_elt){ |
111 if([this.text_elt, this.element].indexOf(e.target.parentNode) == -1){ |
108 e.stopPropagation(); |
112 e.stopPropagation(); |
109 // close menu in case click is outside box |
113 // close menu in case click is outside box |
110 if(e.target !== this.box_elt) |
114 if(e.target !== this.box_elt) |
111 this.close(); |
115 this.close(); |
112 } |
116 } |
113 } |
117 } |
114 close(){ |
118 close(){ |
115 // Stop hogging all click events |
119 // Stop hogging all click events |
|
120 svg_root.removeEventListener("pointerdown", numb_event, true); |
|
121 svg_root.removeEventListener("pointerup", numb_event, true); |
116 svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); |
122 svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); |
117 // Restore position and sixe of widget elements |
123 // Restore position and sixe of widget elements |
118 this.reset_text(); |
124 this.reset_text(); |
|
125 this.reset_clickables(); |
119 this.reset_box(); |
126 this.reset_box(); |
120 // Put the button back in place |
127 // Put the button back in place |
121 this.element.appendChild(this.button_elt); |
128 this.element.appendChild(this.button_elt); |
122 // Mark as closed (to allow dispatch) |
129 // Mark as closed (to allow dispatch) |
123 this.opened = false; |
130 this.opened = false; |
124 // Dispatch last cached value |
131 // Dispatch last cached value |
125 this.apply_cache(); |
132 this.apply_cache(); |
126 } |
133 } |
|
134 // Make item (text span) clickable by overlaying a rectangle on top of it |
|
135 make_clickable(span, func) { |
|
136 let txt = this.text_elt; |
|
137 let first = txt.firstElementChild; |
|
138 let original_text_y = this.text_bbox.y; |
|
139 let highlight = this.highlight_elt; |
|
140 let original_h_y = highlight.getBBox().y; |
|
141 let clickable = highlight.cloneNode(); |
|
142 let yoffset = span.getBBox().y - original_text_y; |
|
143 clickable.setAttribute("y", original_h_y + yoffset); |
|
144 clickable.style.pointerEvents = "bounding-box"; |
|
145 clickable.style.visibility = "hidden"; |
|
146 //clickable.onclick = () => alert("love JS"); |
|
147 clickable.onclick = func; |
|
148 this.element.appendChild(clickable); |
|
149 this.clickables.push(clickable) |
|
150 } |
|
151 reset_clickables() { |
|
152 while(this.clickables.length){ |
|
153 this.element.removeChild(this.clickables.pop()); |
|
154 } |
|
155 } |
127 // Set text content when content is smaller than menu (no scrolling) |
156 // Set text content when content is smaller than menu (no scrolling) |
128 set_complete_text(){ |
157 set_complete_text(){ |
129 let spans = this.text_elt.children; |
158 let spans = this.text_elt.children; |
130 let c = 0; |
159 let c = 0; |
131 for(let item of this.content){ |
160 for(let item of this.content){ |
132 let span=spans[c]; |
161 let span=spans[c]; |
133 span.textContent = item; |
162 span.textContent = item; |
134 let sel = c; |
163 let sel = c; |
135 span.onclick = (evt) => this.bound_on_selection_click(sel); |
164 this.make_clickable(span, (evt) => this.bound_on_selection_click(sel)); |
136 c++; |
165 c++; |
137 } |
166 } |
138 } |
167 } |
139 // Move partial view : |
168 // Move partial view : |
140 // false : upward, lower value |
169 // false : upward, lower value |
141 // true : downward, higher value |
170 // true : downward, higher value |
142 scroll(forward){ |
171 scroll(forward){ |
143 let contentlength = this.content.length; |
172 let contentlength = this.content.length; |
144 let spans = this.text_elt.children; |
173 let spans = this.text_elt.children; |
145 let spanslength = spans.length; |
174 let spanslength = spans.length; |
146 // reduce accounted menu size according to jumps |
175 // reduce accounted menu size according to jumps |
147 if(this.menu_offset != 0) spanslength--; |
176 if(this.menu_offset != 0) spanslength--; |
148 if(this.menu_offset < contentlength - 1) spanslength--; |
177 if(this.menu_offset < contentlength - 1) spanslength--; |
149 if(forward){ |
178 if(forward){ |
150 this.menu_offset = Math.min( |
179 this.menu_offset = Math.min( |
151 contentlength - spans.length + 1, |
180 contentlength - spans.length + 1, |
152 this.menu_offset + spanslength); |
181 this.menu_offset + spanslength); |
153 }else{ |
182 }else{ |
154 this.menu_offset = Math.max( |
183 this.menu_offset = Math.max( |
155 0, |
184 0, |
156 this.menu_offset - spanslength); |
185 this.menu_offset - spanslength); |
157 } |
186 } |
|
187 this.reset_clickables(); |
158 this.set_partial_text(); |
188 this.set_partial_text(); |
159 } |
189 } |
160 // Setup partial view text content |
190 // Setup partial view text content |
161 // with jumps at first and last entry when appropriate |
191 // with jumps at first and last entry when appropriate |
162 set_partial_text(){ |
192 set_partial_text(){ |
163 let spans = this.text_elt.children; |
193 let spans = this.text_elt.children; |
164 let contentlength = this.content.length; |
194 let contentlength = this.content.length; |
165 let spanslength = spans.length; |
195 let spanslength = spans.length; |
166 let i = this.menu_offset, c = 0; |
196 let i = this.menu_offset, c = 0; |
|
197 let m = this.box_bbox; |
167 while(c < spanslength){ |
198 while(c < spanslength){ |
168 let span=spans[c]; |
199 let span=spans[c]; |
|
200 let onclickfunc; |
169 // backward jump only present if not exactly at start |
201 // backward jump only present if not exactly at start |
170 if(c == 0 && i != 0){ |
202 if(c == 0 && i != 0){ |
171 span.textContent = "↑ ↑ ↑"; |
203 span.textContent = "▲"; |
172 span.onclick = this.bound_on_backward_click; |
204 onclickfunc = this.bound_on_backward_click; |
|
205 let o = span.getBBox(); |
|
206 span.setAttribute("dx", (m.width - o.width)/2); |
173 // presence of forward jump when not right at the end |
207 // presence of forward jump when not right at the end |
174 }else if(c == spanslength-1 && i < contentlength - 1){ |
208 }else if(c == spanslength-1 && i < contentlength - 1){ |
175 span.textContent = "↓ ↓ ↓"; |
209 span.textContent = "▼"; |
176 span.onclick = this.bound_on_forward_click; |
210 onclickfunc = this.bound_on_forward_click; |
|
211 let o = span.getBBox(); |
|
212 span.setAttribute("dx", (m.width - o.width)/2); |
177 // otherwise normal content |
213 // otherwise normal content |
178 }else{ |
214 }else{ |
179 span.textContent = this.content[i]; |
215 span.textContent = this.content[i]; |
180 let sel = i; |
216 let sel = i; |
181 span.onclick = (evt) => this.bound_on_selection_click(sel); |
217 onclickfunc = (evt) => this.bound_on_selection_click(sel); |
|
218 span.removeAttribute("dx"); |
182 i++; |
219 i++; |
183 } |
220 } |
|
221 this.make_clickable(span, onclickfunc); |
184 c++; |
222 c++; |
185 } |
223 } |
186 } |
224 } |
187 open(){ |
225 open(){ |
188 let length = this.content.length; |
226 let length = this.content.length; |
209 // Take button out until menu closed |
247 // Take button out until menu closed |
210 this.element.removeChild(this.button_elt); |
248 this.element.removeChild(this.button_elt); |
211 // Rise widget to top by moving it to last position among siblings |
249 // Rise widget to top by moving it to last position among siblings |
212 this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); |
250 this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); |
213 // disable interaction with background |
251 // disable interaction with background |
|
252 svg_root.addEventListener("pointerdown", numb_event, true); |
|
253 svg_root.addEventListener("pointerup", numb_event, true); |
214 svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); |
254 svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); |
215 // mark as open |
255 // mark as open |
216 this.opened = true; |
256 this.opened = true; |
217 } |
257 } |
218 // Put text element in normalized state |
258 // Put text element in normalized state |
219 reset_text(){ |
259 reset_text(){ |
220 let txt = this.text_elt; |
260 let txt = this.text_elt; |
221 let first = txt.firstElementChild; |
261 let first = txt.firstElementChild; |
222 // remove attribute eventually added to first text line while opening |
262 // remove attribute eventually added to first text line while opening |
223 first.removeAttribute("onclick"); |
263 first.onclick = null; |
224 first.removeAttribute("dy"); |
264 first.removeAttribute("dy"); |
|
265 first.removeAttribute("dx"); |
225 // keep only the first line of text |
266 // keep only the first line of text |
226 for(let span of Array.from(txt.children).slice(1)){ |
267 for(let span of Array.from(txt.children).slice(1)){ |
227 txt.removeChild(span) |
268 txt.removeChild(span) |
228 } |
269 } |
229 } |
270 } |
239 // Use margin and text size to compute box size |
280 // Use margin and text size to compute box size |
240 adjust_box_to_text(){ |
281 adjust_box_to_text(){ |
241 let [lmargin, tmargin] = this.margins; |
282 let [lmargin, tmargin] = this.margins; |
242 let m = this.text_elt.getBBox(); |
283 let m = this.text_elt.getBBox(); |
243 let b = this.box_elt; |
284 let b = this.box_elt; |
244 b.x.baseVal.value = m.x - lmargin; |
285 // b.x.baseVal.value = m.x - lmargin; |
245 b.y.baseVal.value = m.y - tmargin; |
286 b.y.baseVal.value = m.y - tmargin; |
246 b.width.baseVal.value = 2 * lmargin + m.width; |
287 // b.width.baseVal.value = 2 * lmargin + m.width; |
247 b.height.baseVal.value = 2 * tmargin + m.height; |
288 b.height.baseVal.value = 2 * tmargin + m.height; |
248 } |
289 } |
249 } |
290 } |
250 || |
291 || |
251 } |
292 } |
|
293 |
252 template "widget[@type='DropDown']", mode="widget_defs" { |
294 template "widget[@type='DropDown']", mode="widget_defs" { |
253 param "hmi_element"; |
295 param "hmi_element"; |
254 labels("text box button"); |
296 labels("text box button highlight"); |
255 || |
297 || |
256 // It is assumed that list content conforms to Array interface. |
298 // It is assumed that list content conforms to Array interface. |
257 content: [ |
299 content: [ |
258 ``foreach "arg" | "«@value»", |
300 ``foreach "arg" | "«@value»", |
259 ], |
301 ], |