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