1 // widget_dropdown.ysl2 |
1 // widget_dropdown.ysl2 |
2 |
2 |
3 template "widget[@type='DropDown']", mode="widget_defs" { |
3 template "widget[@type='DropDown']", mode="widget_defs" { |
4 param "hmi_element"; |
4 param "hmi_element"; |
5 labels("text box button"); |
5 labels("text box button"); |
6 || |
6 || |
7 dispatch: function(value) { |
7 dispatch: function(value) { |
8 if(!this.opened) this.set_selection(value); |
8 if(!this.opened) this.set_selection(value); |
9 }, |
9 }, |
10 init: function() { |
10 init: function() { |
11 this.button_elt.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_button_click()"); |
11 this.button_elt.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_button_click()"); |
12 this.text_bbox = this.text_elt.getBBox() |
12 this.text_bbox = this.text_elt.getBBox() |
13 this.box_bbox = this.box_elt.getBBox() |
13 this.box_bbox = this.box_elt.getBBox() |
14 lmargin = this.text_bbox.x - this.box_bbox.x; |
14 lmargin = this.text_bbox.x - this.box_bbox.x; |
15 tmargin = this.text_bbox.y - this.box_bbox.y; |
15 tmargin = this.text_bbox.y - this.box_bbox.y; |
16 this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); |
16 this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); |
17 //this.content = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", |
17 |
18 // "eleven", "twelve", "thirteen", "fourteen", "fifteen"]; |
18 // It is assumed that list content conforms to Array interface. |
19 this.content = [ |
19 this.content = [ |
20 ``foreach "arg" | "«@value»", |
20 ``foreach "arg" | "«@value»", |
21 ]; |
21 ]; |
|
22 |
|
23 // Index of first visible element in the menu, when opened |
22 this.menu_offset = 0; |
24 this.menu_offset = 0; |
|
25 |
|
26 // How mutch to lift the menu vertically so that it does not cross bottom border |
23 this.lift = 0; |
27 this.lift = 0; |
|
28 |
|
29 // Event handlers cannot be object method ('this' is unknown) |
|
30 // as a workaround, handler given to addEventListener is bound in advance. |
|
31 this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this); |
|
32 |
24 this.opened = false; |
33 this.opened = false; |
25 this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this); |
34 }, |
26 }, |
35 // Called when a menu entry is clicked |
27 on_selection_click: function(selection) { |
36 on_selection_click: function(selection) { |
28 console.log("selected "+selection); |
|
29 this.close(); |
37 this.close(); |
30 let orig = this.indexes[0]; |
38 let orig = this.indexes[0]; |
31 let idx = this.offset ? orig - this.offset : orig; |
39 let idx = this.offset ? orig - this.offset : orig; |
32 apply_hmi_value(idx, selection); |
40 apply_hmi_value(idx, selection); |
33 }, |
41 }, |
34 on_button_click: function() { |
42 on_button_click: function() { |
35 this.open(); |
43 this.open(); |
36 }, |
44 }, |
37 on_backward_click:function(){ |
45 on_backward_click: function(){ |
38 this.move(false); |
46 this.scroll(false); |
39 }, |
47 }, |
40 on_forward_click:function(){ |
48 on_forward_click:function(){ |
41 this.move(true); |
49 this.scroll(true); |
42 }, |
50 }, |
43 set_selection: function(value) { |
51 set_selection: function(value) { |
44 let display_str; |
52 let display_str; |
45 if(value >= 0 && value < this.content.length){ |
53 if(value >= 0 && value < this.content.length){ |
|
54 // if valid selection resolve content |
46 display_str = this.content[value]; |
55 display_str = this.content[value]; |
47 this.last_selection = value; |
56 this.last_selection = value; |
48 } else { |
57 } else { |
|
58 // otherwise show problem |
49 display_str = "?"+String(value)+"?"; |
59 display_str = "?"+String(value)+"?"; |
50 } |
60 } |
|
61 // It is assumed that first span always stays, |
|
62 // and contains selection when menu is closed |
51 this.text_elt.firstElementChild.textContent = display_str; |
63 this.text_elt.firstElementChild.textContent = display_str; |
52 }, |
64 }, |
53 grow_text: function(up_to) { |
65 grow_text: function(up_to) { |
54 let count = 1; |
66 let count = 1; |
55 let txt = this.text_elt; |
67 let txt = this.text_elt; |
56 let first = txt.firstElementChild; |
68 let first = txt.firstElementChild; |
|
69 // Real world (pixels) boundaries of current page |
57 let bounds = svg_root.getBoundingClientRect(); |
70 let bounds = svg_root.getBoundingClientRect(); |
58 this.lift = 0; |
71 this.lift = 0; |
59 while(count < up_to) { |
72 while(count < up_to) { |
60 let next = first.cloneNode(); |
73 let next = first.cloneNode(); |
|
74 // relative line by line text flow instead of absolute y coordinate |
61 next.removeAttribute("y"); |
75 next.removeAttribute("y"); |
62 next.setAttribute("dy", "1.1em"); |
76 next.setAttribute("dy", "1.1em"); |
|
77 // default content to allow computing text element bbox |
63 next.textContent = "..."; |
78 next.textContent = "..."; |
|
79 // append new span to text element |
64 txt.appendChild(next); |
80 txt.appendChild(next); |
|
81 // now check if text extended by one row fits to page |
|
82 // FIXME : exclude margins to be more accurate on box size |
65 let rect = txt.getBoundingClientRect(); |
83 let rect = txt.getBoundingClientRect(); |
66 if(rect.bottom > bounds.bottom){ |
84 if(rect.bottom > bounds.bottom){ |
|
85 // in case of overflow at the bottom, lift up one row |
67 let backup = first.getAttribute("dy"); |
86 let backup = first.getAttribute("dy"); |
|
87 // apply lift asr a dy added too first span (y attrib stays) |
68 first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em"); |
88 first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em"); |
69 rect = txt.getBoundingClientRect(); |
89 rect = txt.getBoundingClientRect(); |
70 if(rect.top > bounds.top){ |
90 if(rect.top > bounds.top){ |
71 this.lift += 1; |
91 this.lift += 1; |
72 } else { |
92 } else { |
|
93 // if it goes over the top, then backtrack |
|
94 // restore dy attribute on first span |
73 if(backup) |
95 if(backup) |
74 first.setAttribute("dy", backup); |
96 first.setAttribute("dy", backup); |
75 else |
97 else |
76 first.removeAttribute("dy"); |
98 first.removeAttribute("dy"); |
|
99 // remove unwanted child |
77 txt.removeChild(next); |
100 txt.removeChild(next); |
78 return count; |
101 return count; |
79 } |
102 } |
80 } |
103 } |
81 count++; |
104 count++; |
82 } |
105 } |
83 return count; |
106 return count; |
84 }, |
107 }, |
85 close_on_click_elsewhere: function(e) { |
108 close_on_click_elsewhere: function(e) { |
86 console.log("inhibit", e); |
109 // inhibit events not targetting spans (menu items) |
87 console.log(e.target.parentNode, this.text_elt); |
|
88 if(e.target.parentNode !== this.text_elt){ |
110 if(e.target.parentNode !== this.text_elt){ |
89 e.stopPropagation(); |
111 e.stopPropagation(); |
|
112 // close menu in case click is outside box |
90 if(e.target !== this.box_elt) |
113 if(e.target !== this.box_elt) |
91 this.close(); |
114 this.close(); |
92 } |
115 } |
93 }, |
116 }, |
94 close: function(){ |
117 close: function(){ |
95 document.removeEventListener("click", this.bound_close_on_click_elsewhere, true); |
118 // Stop hogging all click events |
|
119 svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); |
|
120 // Restore position and sixe of widget elements |
96 this.reset_text(); |
121 this.reset_text(); |
97 this.reset_box(); |
122 this.reset_box(); |
|
123 // Put the button back in place |
98 this.element.appendChild(this.button_elt); |
124 this.element.appendChild(this.button_elt); |
|
125 // Mark as closed (to allow dispatch) |
99 this.opened = false; |
126 this.opened = false; |
|
127 // Dispatch last cached value |
100 this.apply_cache(); |
128 this.apply_cache(); |
101 }, |
129 }, |
|
130 // Set text content when content is smaller than menu (no scrolling) |
102 set_complete_text: function(){ |
131 set_complete_text: function(){ |
103 let spans = this.text_elt.children; |
132 let spans = this.text_elt.children; |
104 let c = 0; |
133 let c = 0; |
105 for(let item of this.content){ |
134 for(let item of this.content){ |
106 let span=spans[c]; |
135 let span=spans[c]; |
107 span.textContent = item; |
136 span.textContent = item; |
108 span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_selection_click("+c+")"); |
137 span.setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_selection_click("+c+")"); |
109 c++; |
138 c++; |
110 } |
139 } |
111 }, |
140 }, |
112 move: function(forward){ |
141 // Move partial view : |
|
142 // false : upward, lower value |
|
143 // true : downward, higher value |
|
144 scroll: function(forward){ |
113 let contentlength = this.content.length; |
145 let contentlength = this.content.length; |
114 let spans = this.text_elt.children; |
146 let spans = this.text_elt.children; |
115 let spanslength = spans.length; |
147 let spanslength = spans.length; |
116 if(this.menu_offset != 0) spanslength--; |
148 if(this.menu_offset != 0) spanslength--; |
117 if(this.menu_offset < contentlength - 1) spanslength--; |
149 if(this.menu_offset < contentlength - 1) spanslength--; |
148 c++; |
182 c++; |
149 } |
183 } |
150 }, |
184 }, |
151 open: function(){ |
185 open: function(){ |
152 let length = this.content.length; |
186 let length = this.content.length; |
|
187 // systematically reset text, to strip eventual whitespace spans |
153 this.reset_text(); |
188 this.reset_text(); |
|
189 // grow as much as needed or possible |
154 let slots = this.grow_text(length); |
190 let slots = this.grow_text(length); |
|
191 // Depending on final size |
155 if(slots == length) { |
192 if(slots == length) { |
|
193 // show all at once |
156 this.set_complete_text(); |
194 this.set_complete_text(); |
157 } else { |
195 } else { |
158 // align to selection |
196 // eventualy align menu to current selection, compensating for lift |
159 let offset = this.last_selection - this.lift; |
197 let offset = this.last_selection - this.lift; |
160 if(offset > 0) |
198 if(offset > 0) |
161 this.menu_offset = Math.min(offset + 1, length - slots + 1); |
199 this.menu_offset = Math.min(offset + 1, length - slots + 1); |
162 else |
200 else |
163 this.menu_offset = 0; |
201 this.menu_offset = 0; |
|
202 // show surrounding values |
164 this.set_partial_text(); |
203 this.set_partial_text(); |
165 } |
204 } |
|
205 // Now that text size is known, we can set the box around it |
166 this.adjust_box_to_text(); |
206 this.adjust_box_to_text(); |
|
207 // Take button out until menu closed |
167 this.element.removeChild(this.button_elt); |
208 this.element.removeChild(this.button_elt); |
|
209 // Place widget in front by moving it to last position among siblings |
168 this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); |
210 this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); |
169 // disable interaction with background |
211 // disable interaction with background |
170 document.addEventListener("click", this.bound_close_on_click_elsewhere, true); |
212 svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); |
171 this.opened = true; |
213 this.opened = true; |
172 }, |
214 }, |
173 reset_text: function(){ |
215 reset_text: function(){ |
174 let txt = this.text_elt; |
216 let txt = this.text_elt; |
175 let first = txt.firstElementChild; |
217 let first = txt.firstElementChild; |