##// END OF EJS Templates
Use contentEditable to allow modification via the the slider readout
Gordon Ball -
Show More
@@ -1,373 +1,405
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "widgets/js/widget",
6 6 "jqueryui",
7 7 "bootstrap",
8 8 ], function(widget, $){
9 9
10 10 var IntSliderView = widget.DOMWidgetView.extend({
11 11 render : function(){
12 12 // Called when view is rendered.
13 13 this.$el
14 14 .addClass('widget-hbox');
15 15 this.$label = $('<div />')
16 16 .appendTo(this.$el)
17 17 .addClass('widget-label')
18 18 .hide();
19 19
20 20 this.$slider = $('<div />')
21 21 .slider({})
22 22 .addClass('slider');
23 23 // Put the slider in a container
24 24 this.$slider_container = $('<div />')
25 25 .addClass('widget-hslider')
26 26 .append(this.$slider);
27 27 this.$el.append(this.$slider_container);
28 28
29 29 this.$readout = $('<div/>')
30 30 .appendTo(this.$el)
31 31 .addClass('widget-readout')
32 .attr('contentEditable', true)
32 33 .hide();
33 34
34 35 this.model.on('change:slider_color', function(sender, value) {
35 36 this.$slider.find('a').css('background', value);
36 37 }, this);
37 38 this.$slider.find('a').css('background', this.model.get('slider_color'));
38 39
39 40 // Set defaults.
40 41 this.update();
41 42 },
42 43
43 44 update_attr: function(name, value) {
44 45 // Set a css attr of the widget view.
45 46 if (name == 'color') {
46 47 this.$readout.css(name, value);
47 48 } else if (name.substring(0, 4) == 'font') {
48 49 this.$readout.css(name, value);
49 50 } else if (name.substring(0, 6) == 'border') {
50 51 this.$slider.find('a').css(name, value);
51 52 this.$slider_container.css(name, value);
52 53 } else if (name == 'width' || name == 'height' || name == 'background') {
53 54 this.$slider_container.css(name, value);
54 55 } else {
55 56 this.$slider.css(name, value);
56 57 }
57 58 },
58 59
59 60 update : function(options){
60 61 // Update the contents of this view
61 62 //
62 63 // Called when the model is changed. The model may have been
63 64 // changed by another view or by a state update from the back-end.
64 65 if (options === undefined || options.updated_view != this) {
65 66 // JQuery slider option keys. These keys happen to have a
66 67 // one-to-one mapping with the corrosponding keys of the model.
67 68 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
68 69 var that = this;
69 70 that.$slider.slider({});
70 71 _.each(jquery_slider_keys, function(key, i) {
71 72 var model_value = that.model.get(key);
72 73 if (model_value !== undefined) {
73 74 that.$slider.slider("option", key, model_value);
74 75 }
75 76 });
76 77 var range_value = this.model.get("_range");
77 78 if (range_value !== undefined) {
78 79 this.$slider.slider("option", "range", range_value);
79 80 }
80 81
81 82 // WORKAROUND FOR JQUERY SLIDER BUG.
82 83 // The horizontal position of the slider handle
83 84 // depends on the value of the slider at the time
84 85 // of orientation change. Before applying the new
85 86 // workaround, we set the value to the minimum to
86 87 // make sure that the horizontal placement of the
87 88 // handle in the vertical slider is always
88 89 // consistent.
89 90 var orientation = this.model.get('orientation');
90 91 var min = this.model.get('min');
91 92 var max = this.model.get('max');
92 93 if (this.model.get('_range')) {
93 94 this.$slider.slider('option', 'values', [min, min]);
94 95 } else {
95 96 this.$slider.slider('option', 'value', min);
96 97 }
97 98 this.$slider.slider('option', 'orientation', orientation);
98 99 var value = this.model.get('value');
99 100 if (this.model.get('_range')) {
100 101 // values for the range case are validated python-side in
101 102 // _Bounded{Int,Float}RangeWidget._validate
102 103 this.$slider.slider('option', 'values', value);
103 104 this.$readout.text(value.join("-"));
104 105 } else {
105 106 if(value > max) {
106 107 value = max;
107 108 }
108 109 else if(value < min){
109 110 value = min;
110 111 }
111 112 this.$slider.slider('option', 'value', value);
112 113 this.$readout.text(value);
113 114 }
114 115
115 116 if(this.model.get('value')!=value) {
116 117 this.model.set('value', value, {updated_view: this});
117 118 this.touch();
118 119 }
119 120
120 121 // Use the right CSS classes for vertical & horizontal sliders
121 122 if (orientation=='vertical') {
122 123 this.$slider_container
123 124 .removeClass('widget-hslider')
124 125 .addClass('widget-vslider');
125 126 this.$el
126 127 .removeClass('widget-hbox')
127 128 .addClass('widget-vbox');
128 129
129 130 } else {
130 131 this.$slider_container
131 132 .removeClass('widget-vslider')
132 133 .addClass('widget-hslider');
133 134 this.$el
134 135 .removeClass('widget-vbox')
135 136 .addClass('widget-hbox');
136 137 }
137 138
138 139 var description = this.model.get('description');
139 140 if (description.length === 0) {
140 141 this.$label.hide();
141 142 } else {
142 143 this.$label.text(description);
143 144 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
144 145 this.$label.show();
145 146 }
146 147
147 148 var readout = this.model.get('readout');
148 149 if (readout) {
149 150 this.$readout.show();
150 151 } else {
151 152 this.$readout.hide();
152 153 }
153 154 }
154 155 return IntSliderView.__super__.update.apply(this);
155 156 },
156 157
157 158 events: {
158 159 // Dictionary of events and their handlers.
159 "slide" : "handleSliderChange"
160 "slide" : "handleSliderChange",
161 "blur [contentEditable=true]": "handleTextChange"
162 },
163
164 handleTextChange: function(e) {
165 var text = $(e.target).text().trim();
166 var value = this._validate_text_input(text);
167 if (isNaN(value)) {
168 this.$readout.text(this.model.get('value'));
169 } else {
170 //check for outside range
171 if (value > this.model.get('max')) value = this.model.get('max');
172 if (value < this.model.get('min')) value = this.model.get('min');
173
174 //update the readout unconditionally
175 //this covers eg, entering a float value which rounds to the
176 //existing int value, which will not trigger an update since the model
177 //doesn't change, but we should update the text to reflect that
178 //a float value isn't being used
179 this.$readout.text(value);
180
181 //note that the step size currently isn't enforced, so if an
182 //off-step value is input it will be retained
183
184 //update the model
185 this.model.set('value', value, {updated_view: this});
186 this.touch();
187 }
188 },
189
190 _validate_text_input: function(x) {
191 return parseInt(x);
160 192 },
161 193
162 194 handleSliderChange: function(e, ui) {
163 195 // Called when the slider value is changed.
164 196
165 197 // Calling model.set will trigger all of the other views of the
166 198 // model to update.
167 199 if (this.model.get("_range")) {
168 200 var actual_value = ui.values.map(this._validate_slide_value);
169 201 this.$readout.text(actual_value.join("-"));
170 202 } else {
171 203 var actual_value = this._validate_slide_value(ui.value);
172 204 this.$readout.text(actual_value);
173 205 }
174 206 this.model.set('value', actual_value, {updated_view: this});
175 207 this.touch();
176 208 },
177 209
178 210 _validate_slide_value: function(x) {
179 211 // Validate the value of the slider before sending it to the back-end
180 212 // and applying it to the other views on the page.
181 213
182 214 // Double bit-wise not truncates the decimel (int cast).
183 215 return ~~x;
184 216 },
185 217 });
186 218
187 219
188 220 var IntTextView = widget.DOMWidgetView.extend({
189 221 render : function(){
190 222 // Called when view is rendered.
191 223 this.$el
192 224 .addClass('widget-hbox');
193 225 this.$label = $('<div />')
194 226 .appendTo(this.$el)
195 227 .addClass('widget-label')
196 228 .hide();
197 229 this.$textbox = $('<input type="text" />')
198 230 .addClass('form-control')
199 231 .addClass('widget-numeric-text')
200 232 .appendTo(this.$el);
201 233 this.update(); // Set defaults.
202 234 },
203 235
204 236 update : function(options){
205 237 // Update the contents of this view
206 238 //
207 239 // Called when the model is changed. The model may have been
208 240 // changed by another view or by a state update from the back-end.
209 241 if (options === undefined || options.updated_view != this) {
210 242 var value = this.model.get('value');
211 243 if (this._parse_value(this.$textbox.val()) != value) {
212 244 this.$textbox.val(value);
213 245 }
214 246
215 247 if (this.model.get('disabled')) {
216 248 this.$textbox.attr('disabled','disabled');
217 249 } else {
218 250 this.$textbox.removeAttr('disabled');
219 251 }
220 252
221 253 var description = this.model.get('description');
222 254 if (description.length === 0) {
223 255 this.$label.hide();
224 256 } else {
225 257 this.$label.text(description);
226 258 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
227 259 this.$label.show();
228 260 }
229 261 }
230 262 return IntTextView.__super__.update.apply(this);
231 263 },
232 264
233 265 update_attr: function(name, value) {
234 266 // Set a css attr of the widget view.
235 267 this.$textbox.css(name, value);
236 268 },
237 269
238 270 events: {
239 271 // Dictionary of events and their handlers.
240 272 "keyup input" : "handleChanging",
241 273 "paste input" : "handleChanging",
242 274 "cut input" : "handleChanging",
243 275
244 276 // Fires only when control is validated or looses focus.
245 277 "change input" : "handleChanged"
246 278 },
247 279
248 280 handleChanging: function(e) {
249 281 // Handles and validates user input.
250 282
251 283 // Try to parse value as a int.
252 284 var numericalValue = 0;
253 285 if (e.target.value !== '') {
254 286 var trimmed = e.target.value.trim();
255 287 if (!(['-', '-.', '.', '+.', '+'].indexOf(trimmed) >= 0)) {
256 288 numericalValue = this._parse_value(e.target.value);
257 289 }
258 290 }
259 291
260 292 // If parse failed, reset value to value stored in model.
261 293 if (isNaN(numericalValue)) {
262 294 e.target.value = this.model.get('value');
263 295 } else if (!isNaN(numericalValue)) {
264 296 if (this.model.get('max') !== undefined) {
265 297 numericalValue = Math.min(this.model.get('max'), numericalValue);
266 298 }
267 299 if (this.model.get('min') !== undefined) {
268 300 numericalValue = Math.max(this.model.get('min'), numericalValue);
269 301 }
270 302
271 303 // Apply the value if it has changed.
272 304 if (numericalValue != this.model.get('value')) {
273 305
274 306 // Calling model.set will trigger all of the other views of the
275 307 // model to update.
276 308 this.model.set('value', numericalValue, {updated_view: this});
277 309 this.touch();
278 310 }
279 311 }
280 312 },
281 313
282 314 handleChanged: function(e) {
283 315 // Applies validated input.
284 316 if (this.model.get('value') != e.target.value) {
285 317 e.target.value = this.model.get('value');
286 318 }
287 319 },
288 320
289 321 _parse_value: function(value) {
290 322 // Parse the value stored in a string.
291 323 return parseInt(value);
292 324 },
293 325 });
294 326
295 327
296 328 var ProgressView = widget.DOMWidgetView.extend({
297 329 render : function(){
298 330 // Called when view is rendered.
299 331 this.$el
300 332 .addClass('widget-hbox');
301 333 this.$label = $('<div />')
302 334 .appendTo(this.$el)
303 335 .addClass('widget-label')
304 336 .hide();
305 337 this.$progress = $('<div />')
306 338 .addClass('progress')
307 339 .addClass('widget-progress')
308 340 .appendTo(this.$el);
309 341 this.$bar = $('<div />')
310 342 .addClass('progress-bar')
311 343 .css('width', '50%')
312 344 .appendTo(this.$progress);
313 345 this.update(); // Set defaults.
314 346
315 347 this.model.on('change:bar_style', function(model, value) {
316 348 this.update_bar_style();
317 349 }, this);
318 350 this.update_bar_style('');
319 351 },
320 352
321 353 update : function(){
322 354 // Update the contents of this view
323 355 //
324 356 // Called when the model is changed. The model may have been
325 357 // changed by another view or by a state update from the back-end.
326 358 var value = this.model.get('value');
327 359 var max = this.model.get('max');
328 360 var min = this.model.get('min');
329 361 var percent = 100.0 * (value - min) / (max - min);
330 362 this.$bar.css('width', percent + '%');
331 363
332 364 var description = this.model.get('description');
333 365 if (description.length === 0) {
334 366 this.$label.hide();
335 367 } else {
336 368 this.$label.text(description);
337 369 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
338 370 this.$label.show();
339 371 }
340 372 return ProgressView.__super__.update.apply(this);
341 373 },
342 374
343 375 update_bar_style: function(previous_trait_value) {
344 376 var class_map = {
345 377 success: ['progress-bar-success'],
346 378 info: ['progress-bar-info'],
347 379 warning: ['progress-bar-warning'],
348 380 danger: ['progress-bar-danger']
349 381 };
350 382 this.update_mapped_classes(class_map, 'bar_style', previous_trait_value, this.$bar);
351 383 },
352 384
353 385 update_attr: function(name, value) {
354 386 // Set a css attr of the widget view.
355 387 if (name.substring(0, 6) == 'border' || name == 'width' ||
356 388 name == 'height' || name == 'background' || name == 'margin' ||
357 389 name == 'padding') {
358 390
359 391 this.$progress.css(name, value);
360 392 } else if (name == 'color') {
361 393 this.$bar.css('background', value);
362 394 } else {
363 395 this.$bar.css(name, value);
364 396 }
365 397 },
366 398 });
367 399
368 400 return {
369 401 'IntSliderView': IntSliderView,
370 402 'IntTextView': IntTextView,
371 403 'ProgressView': ProgressView,
372 404 };
373 405 });
General Comments 0
You need to be logged in to leave comments. Login now