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