##// END OF EJS Templates
Merge pull request #6226 from jasongrout/css-top-default...
Jonathan Frederic -
r17559:802e00b9 merge
parent child Browse files
Show More
@@ -1,494 +1,486 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define(["widgets/js/manager",
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/namespace",
9 9 ], function(widgetmanager, _, Backbone, $, IPython){
10 10
11 11 var WidgetModel = Backbone.Model.extend({
12 12 constructor: function (widget_manager, model_id, comm) {
13 13 // Constructor
14 14 //
15 15 // Creates a WidgetModel instance.
16 16 //
17 17 // Parameters
18 18 // ----------
19 19 // widget_manager : WidgetManager instance
20 20 // model_id : string
21 21 // An ID unique to this model.
22 22 // comm : Comm instance (optional)
23 23 this.widget_manager = widget_manager;
24 24 this._buffered_state_diff = {};
25 25 this.pending_msgs = 0;
26 26 this.msg_buffer = null;
27 27 this.key_value_lock = null;
28 28 this.id = model_id;
29 29 this.views = [];
30 30
31 31 if (comm !== undefined) {
32 32 // Remember comm associated with the model.
33 33 this.comm = comm;
34 34 comm.model = this;
35 35
36 36 // Hook comm messages up to model.
37 37 comm.on_close($.proxy(this._handle_comm_closed, this));
38 38 comm.on_msg($.proxy(this._handle_comm_msg, this));
39 39 }
40 40 return Backbone.Model.apply(this);
41 41 },
42 42
43 43 send: function (content, callbacks) {
44 44 // Send a custom msg over the comm.
45 45 if (this.comm !== undefined) {
46 46 var data = {method: 'custom', content: content};
47 47 this.comm.send(data, callbacks);
48 48 this.pending_msgs++;
49 49 }
50 50 },
51 51
52 52 _handle_comm_closed: function (msg) {
53 53 // Handle when a widget is closed.
54 54 this.trigger('comm:close');
55 55 delete this.comm.model; // Delete ref so GC will collect widget model.
56 56 delete this.comm;
57 57 delete this.model_id; // Delete id from model so widget manager cleans up.
58 58 _.each(this.views, function(view, i) {
59 59 view.remove();
60 60 });
61 61 },
62 62
63 63 _handle_comm_msg: function (msg) {
64 64 // Handle incoming comm msg.
65 65 var method = msg.content.data.method;
66 66 switch (method) {
67 67 case 'update':
68 68 this.apply_update(msg.content.data.state);
69 69 break;
70 70 case 'custom':
71 71 this.trigger('msg:custom', msg.content.data.content);
72 72 break;
73 73 case 'display':
74 74 this.widget_manager.display_view(msg, this);
75 75 break;
76 76 }
77 77 },
78 78
79 79 apply_update: function (state) {
80 80 // Handle when a widget is updated via the python side.
81 81 var that = this;
82 82 _.each(state, function(value, key) {
83 83 that.key_value_lock = [key, value];
84 84 try {
85 85 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
86 86 } finally {
87 87 that.key_value_lock = null;
88 88 }
89 89 });
90 90 },
91 91
92 92 _handle_status: function (msg, callbacks) {
93 93 // Handle status msgs.
94 94
95 95 // execution_state : ('busy', 'idle', 'starting')
96 96 if (this.comm !== undefined) {
97 97 if (msg.content.execution_state ==='idle') {
98 98 // Send buffer if this message caused another message to be
99 99 // throttled.
100 100 if (this.msg_buffer !== null &&
101 101 (this.get('msg_throttle') || 3) === this.pending_msgs) {
102 102 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
103 103 this.comm.send(data, callbacks);
104 104 this.msg_buffer = null;
105 105 } else {
106 106 --this.pending_msgs;
107 107 }
108 108 }
109 109 }
110 110 },
111 111
112 112 callbacks: function(view) {
113 113 // Create msg callbacks for a comm msg.
114 114 var callbacks = this.widget_manager.callbacks(view);
115 115
116 116 if (callbacks.iopub === undefined) {
117 117 callbacks.iopub = {};
118 118 }
119 119
120 120 var that = this;
121 121 callbacks.iopub.status = function (msg) {
122 122 that._handle_status(msg, callbacks);
123 123 };
124 124 return callbacks;
125 125 },
126 126
127 127 set: function(key, val, options) {
128 128 // Set a value.
129 129 var return_value = WidgetModel.__super__.set.apply(this, arguments);
130 130
131 131 // Backbone only remembers the diff of the most recent set()
132 132 // operation. Calling set multiple times in a row results in a
133 133 // loss of diff information. Here we keep our own running diff.
134 134 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
135 135 return return_value;
136 136 },
137 137
138 138 sync: function (method, model, options) {
139 139 // Handle sync to the back-end. Called when a model.save() is called.
140 140
141 141 // Make sure a comm exists.
142 142 var error = options.error || function() {
143 143 console.error('Backbone sync error:', arguments);
144 144 };
145 145 if (this.comm === undefined) {
146 146 error();
147 147 return false;
148 148 }
149 149
150 150 // Delete any key value pairs that the back-end already knows about.
151 151 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
152 152 if (this.key_value_lock !== null) {
153 153 var key = this.key_value_lock[0];
154 154 var value = this.key_value_lock[1];
155 155 if (attrs[key] === value) {
156 156 delete attrs[key];
157 157 }
158 158 }
159 159
160 160 // Only sync if there are attributes to send to the back-end.
161 161 attrs = this._pack_models(attrs);
162 162 if (_.size(attrs) > 0) {
163 163
164 164 // If this message was sent via backbone itself, it will not
165 165 // have any callbacks. It's important that we create callbacks
166 166 // so we can listen for status messages, etc...
167 167 var callbacks = options.callbacks || this.callbacks();
168 168
169 169 // Check throttle.
170 170 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
171 171 // The throttle has been exceeded, buffer the current msg so
172 172 // it can be sent once the kernel has finished processing
173 173 // some of the existing messages.
174 174
175 175 // Combine updates if it is a 'patch' sync, otherwise replace updates
176 176 switch (method) {
177 177 case 'patch':
178 178 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
179 179 break;
180 180 case 'update':
181 181 case 'create':
182 182 this.msg_buffer = attrs;
183 183 break;
184 184 default:
185 185 error();
186 186 return false;
187 187 }
188 188 this.msg_buffer_callbacks = callbacks;
189 189
190 190 } else {
191 191 // We haven't exceeded the throttle, send the message like
192 192 // normal.
193 193 var data = {method: 'backbone', sync_data: attrs};
194 194 this.comm.send(data, callbacks);
195 195 this.pending_msgs++;
196 196 }
197 197 }
198 198 // Since the comm is a one-way communication, assume the message
199 199 // arrived. Don't call success since we don't have a model back from the server
200 200 // this means we miss out on the 'sync' event.
201 201 this._buffered_state_diff = {};
202 202 },
203 203
204 204 save_changes: function(callbacks) {
205 205 // Push this model's state to the back-end
206 206 //
207 207 // This invokes a Backbone.Sync.
208 208 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
209 209 },
210 210
211 211 _pack_models: function(value) {
212 212 // Replace models with model ids recursively.
213 213 var that = this;
214 214 var packed;
215 215 if (value instanceof Backbone.Model) {
216 216 return "IPY_MODEL_" + value.id;
217 217
218 218 } else if ($.isArray(value)) {
219 219 packed = [];
220 220 _.each(value, function(sub_value, key) {
221 221 packed.push(that._pack_models(sub_value));
222 222 });
223 223 return packed;
224 224
225 225 } else if (value instanceof Object) {
226 226 packed = {};
227 227 _.each(value, function(sub_value, key) {
228 228 packed[key] = that._pack_models(sub_value);
229 229 });
230 230 return packed;
231 231
232 232 } else {
233 233 return value;
234 234 }
235 235 },
236 236
237 237 _unpack_models: function(value) {
238 238 // Replace model ids with models recursively.
239 239 var that = this;
240 240 var unpacked;
241 241 if ($.isArray(value)) {
242 242 unpacked = [];
243 243 _.each(value, function(sub_value, key) {
244 244 unpacked.push(that._unpack_models(sub_value));
245 245 });
246 246 return unpacked;
247 247
248 248 } else if (value instanceof Object) {
249 249 unpacked = {};
250 250 _.each(value, function(sub_value, key) {
251 251 unpacked[key] = that._unpack_models(sub_value);
252 252 });
253 253 return unpacked;
254 254
255 255 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
256 256 var model = this.widget_manager.get_model(value.slice(10, value.length));
257 257 if (model) {
258 258 return model;
259 259 } else {
260 260 return value;
261 261 }
262 262 } else {
263 263 return value;
264 264 }
265 265 },
266 266
267 267 });
268 268 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
269 269
270 270
271 271 var WidgetView = Backbone.View.extend({
272 272 initialize: function(parameters) {
273 273 // Public constructor.
274 274 this.model.on('change',this.update,this);
275 275 this.options = parameters.options;
276 276 this.child_model_views = {};
277 277 this.child_views = {};
278 278 this.model.views.push(this);
279 279 this.id = this.id || IPython.utils.uuid();
280 280 this.on('displayed', function() {
281 281 this.is_displayed = true;
282 282 }, this);
283 283 },
284 284
285 285 update: function(){
286 286 // Triggered on model change.
287 287 //
288 288 // Update view to be consistent with this.model
289 289 },
290 290
291 291 create_child_view: function(child_model, options) {
292 292 // Create and return a child view.
293 293 //
294 294 // -given a model and (optionally) a view name if the view name is
295 295 // not given, it defaults to the model's default view attribute.
296 296
297 297 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
298 298 // it would be great to have the widget manager add the cell metadata
299 299 // to the subview without having to add it here.
300 300 options = $.extend({ parent: this }, options || {});
301 301 var child_view = this.model.widget_manager.create_view(child_model, options, this);
302 302
303 303 // Associate the view id with the model id.
304 304 if (this.child_model_views[child_model.id] === undefined) {
305 305 this.child_model_views[child_model.id] = [];
306 306 }
307 307 this.child_model_views[child_model.id].push(child_view.id);
308 308
309 309 // Remember the view by id.
310 310 this.child_views[child_view.id] = child_view;
311 311 return child_view;
312 312 },
313 313
314 314 pop_child_view: function(child_model) {
315 315 // Delete a child view that was previously created using create_child_view.
316 316 var view_ids = this.child_model_views[child_model.id];
317 317 if (view_ids !== undefined) {
318 318
319 319 // Only delete the first view in the list.
320 320 var view_id = view_ids[0];
321 321 var view = this.child_views[view_id];
322 322 delete this.child_views[view_id];
323 323 view_ids.splice(0,1);
324 324 child_model.views.pop(view);
325 325
326 326 // Remove the view list specific to this model if it is empty.
327 327 if (view_ids.length === 0) {
328 328 delete this.child_model_views[child_model.id];
329 329 }
330 330 return view;
331 331 }
332 332 return null;
333 333 },
334 334
335 335 do_diff: function(old_list, new_list, removed_callback, added_callback) {
336 336 // Difference a changed list and call remove and add callbacks for
337 337 // each removed and added item in the new list.
338 338 //
339 339 // Parameters
340 340 // ----------
341 341 // old_list : array
342 342 // new_list : array
343 343 // removed_callback : Callback(item)
344 344 // Callback that is called for each item removed.
345 345 // added_callback : Callback(item)
346 346 // Callback that is called for each item added.
347 347
348 348 // Walk the lists until an unequal entry is found.
349 349 var i;
350 350 for (i = 0; i < new_list.length; i++) {
351 351 if (i >= old_list.length || new_list[i] !== old_list[i]) {
352 352 break;
353 353 }
354 354 }
355 355
356 356 // Remove the non-matching items from the old list.
357 357 for (var j = i; j < old_list.length; j++) {
358 358 removed_callback(old_list[j]);
359 359 }
360 360
361 361 // Add the rest of the new list items.
362 362 for (; i < new_list.length; i++) {
363 363 added_callback(new_list[i]);
364 364 }
365 365 },
366 366
367 367 callbacks: function(){
368 368 // Create msg callbacks for a comm msg.
369 369 return this.model.callbacks(this);
370 370 },
371 371
372 372 render: function(){
373 373 // Render the view.
374 374 //
375 375 // By default, this is only called the first time the view is created
376 376 },
377 377
378 378 show: function(){
379 379 // Show the widget-area
380 380 if (this.options && this.options.cell &&
381 381 this.options.cell.widget_area !== undefined) {
382 382 this.options.cell.widget_area.show();
383 383 }
384 384 },
385 385
386 386 send: function (content) {
387 387 // Send a custom msg associated with this view.
388 388 this.model.send(content, this.callbacks());
389 389 },
390 390
391 391 touch: function () {
392 392 this.model.save_changes(this.callbacks());
393 393 },
394 394
395 395 after_displayed: function (callback, context) {
396 396 // Calls the callback right away is the view is already displayed
397 397 // otherwise, register the callback to the 'displayed' event.
398 398 if (this.is_displayed) {
399 399 callback.apply(context);
400 400 } else {
401 401 this.on('displayed', callback, context);
402 402 }
403 403 },
404 404 });
405 405
406 406
407 407 var DOMWidgetView = WidgetView.extend({
408 408 initialize: function (parameters) {
409 409 // Public constructor
410 410 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
411 411 this.on('displayed', this.show, this);
412 412 this.after_displayed(function() {
413 413 this.update_visible(this.model, this.model.get("visible"));
414 414 this.update_css(this.model, this.model.get("_css"));
415 415 }, this);
416 416 this.model.on('msg:custom', this.on_msg, this);
417 417 this.model.on('change:visible', this.update_visible, this);
418 418 this.model.on('change:_css', this.update_css, this);
419 419 },
420 420
421 421 on_msg: function(msg) {
422 422 // Handle DOM specific msgs.
423 423 switch(msg.msg_type) {
424 424 case 'add_class':
425 425 this.add_class(msg.selector, msg.class_list);
426 426 break;
427 427 case 'remove_class':
428 428 this.remove_class(msg.selector, msg.class_list);
429 429 break;
430 430 }
431 431 },
432 432
433 433 add_class: function (selector, class_list) {
434 434 // Add a DOM class to an element.
435 435 this._get_selector_element(selector).addClass(class_list);
436 436 },
437 437
438 438 remove_class: function (selector, class_list) {
439 439 // Remove a DOM class from an element.
440 440 this._get_selector_element(selector).removeClass(class_list);
441 441 },
442 442
443 443 update_visible: function(model, value) {
444 444 // Update visibility
445 445 this.$el.toggle(value);
446 446 },
447 447
448 448 update_css: function (model, css) {
449 449 // Update the css styling of this view.
450 450 var e = this.$el;
451 451 if (css === undefined) {return;}
452 452 for (var i = 0; i < css.length; i++) {
453 453 // Apply the css traits to all elements that match the selector.
454 454 var selector = css[i][0];
455 455 var elements = this._get_selector_element(selector);
456 456 if (elements.length > 0) {
457 457 var trait_key = css[i][1];
458 458 var trait_value = css[i][2];
459 459 elements.css(trait_key ,trait_value);
460 460 }
461 461 }
462 462 },
463 463
464 464 _get_selector_element: function (selector) {
465 465 // Get the elements via the css selector.
466
467 // If the selector is blank, apply the style to the $el_to_style
468 // element. If the $el_to_style element is not defined, use apply
469 // the style to the view's element.
470 466 var elements;
471 467 if (!selector) {
472 if (this.$el_to_style === undefined) {
473 elements = this.$el;
474 } else {
475 elements = this.$el_to_style;
476 }
468 elements = this.$el;
477 469 } else {
478 elements = this.$el.find(selector);
470 elements = this.$el.find(selector).addBack(selector);
479 471 }
480 472 return elements;
481 473 },
482 474 });
483 475
484 476 var widget = {
485 477 'WidgetModel': WidgetModel,
486 478 'WidgetView': WidgetView,
487 479 'DOMWidgetView': DOMWidgetView,
488 480 };
489 481
490 482 // For backwards compatability.
491 483 $.extend(IPython, widget);
492 484
493 485 return widget;
494 486 });
@@ -1,120 +1,119 b''
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 "jquery",
7 7 "bootstrap",
8 8 ], function(widget, $){
9 9
10 10 var CheckboxView = widget.DOMWidgetView.extend({
11 11 render : function(){
12 12 // Called when view is rendered.
13 13 this.$el
14 14 .addClass('widget-hbox-single');
15 15 this.$label = $('<div />')
16 16 .addClass('widget-hlabel')
17 17 .appendTo(this.$el)
18 18 .hide();
19 19 this.$checkbox = $('<input />')
20 20 .attr('type', 'checkbox')
21 21 .appendTo(this.$el)
22 22 .click($.proxy(this.handle_click, this));
23 23
24 this.$el_to_style = this.$checkbox; // Set default element to style
25 24 this.update(); // Set defaults.
26 25 },
27 26
28 27 handle_click: function() {
29 28 // Handles when the checkbox is clicked.
30 29
31 30 // Calling model.set will trigger all of the other views of the
32 31 // model to update.
33 32 var value = this.model.get('value');
34 33 this.model.set('value', ! value, {updated_view: this});
35 34 this.touch();
36 35 },
37 36
38 37 update : function(options){
39 38 // Update the contents of this view
40 39 //
41 40 // Called when the model is changed. The model may have been
42 41 // changed by another view or by a state update from the back-end.
43 42 this.$checkbox.prop('checked', this.model.get('value'));
44 43
45 44 if (options === undefined || options.updated_view != this) {
46 45 var disabled = this.model.get('disabled');
47 46 this.$checkbox.prop('disabled', disabled);
48 47
49 48 var description = this.model.get('description');
50 49 if (description.trim().length === 0) {
51 50 this.$label.hide();
52 51 } else {
53 52 this.$label.text(description);
54 53 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
55 54 this.$label.show();
56 55 }
57 56 }
58 57 return CheckboxView.__super__.update.apply(this);
59 58 },
60 59
61 60 });
62 61
63 62
64 63 var ToggleButtonView = widget.DOMWidgetView.extend({
65 64 render : function() {
66 65 // Called when view is rendered.
67 66 var that = this;
68 67 this.setElement($('<button />')
69 68 .addClass('btn btn-default')
70 69 .attr('type', 'button')
71 70 .on('click', function (e) {
72 71 e.preventDefault();
73 72 that.handle_click();
74 73 }));
75 74
76 75 this.update(); // Set defaults.
77 76 },
78 77
79 78 update : function(options){
80 79 // Update the contents of this view
81 80 //
82 81 // Called when the model is changed. The model may have been
83 82 // changed by another view or by a state update from the back-end.
84 83 if (this.model.get('value')) {
85 84 this.$el.addClass('active');
86 85 } else {
87 86 this.$el.removeClass('active');
88 87 }
89 88
90 89 if (options === undefined || options.updated_view != this) {
91 90
92 91 var disabled = this.model.get('disabled');
93 92 this.$el.prop('disabled', disabled);
94 93
95 94 var description = this.model.get('description');
96 95 if (description.trim().length === 0) {
97 96 this.$el.html("&nbsp;"); // Preserve button height
98 97 } else {
99 98 this.$el.text(description);
100 99 }
101 100 }
102 101 return ToggleButtonView.__super__.update.apply(this);
103 102 },
104 103
105 104 handle_click: function(e) {
106 105 // Handles and validates user input.
107 106
108 107 // Calling model.set will trigger all of the other views of the
109 108 // model to update.
110 109 var value = this.model.get('value');
111 110 this.model.set('value', ! value, {updated_view: this});
112 111 this.touch();
113 112 },
114 113 });
115 114
116 115 return {
117 116 'CheckboxView': CheckboxView,
118 117 'ToggleButtonView': ToggleButtonView,
119 118 };
120 119 });
@@ -1,284 +1,283 b''
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 ContainerView = widget.DOMWidgetView.extend({
11 11 initialize: function(){
12 12 // Public constructor
13 13 ContainerView.__super__.initialize.apply(this, arguments);
14 14 this.update_children([], this.model.get('children'));
15 15 this.model.on('change:children', function(model, value) {
16 16 this.update_children(model.previous('children'), value);
17 17 }, this);
18 18 },
19 19
20 20 render: function(){
21 21 // Called when view is rendered.
22 22 this.$el.addClass('widget-container').addClass('vbox');
23 23 },
24 24
25 25 update_children: function(old_list, new_list) {
26 26 // Called when the children list changes.
27 27 this.do_diff(old_list, new_list,
28 28 $.proxy(this.remove_child_model, this),
29 29 $.proxy(this.add_child_model, this));
30 30 },
31 31
32 32 remove_child_model: function(model) {
33 33 // Called when a model is removed from the children list.
34 34 this.pop_child_view(model).remove();
35 35 },
36 36
37 37 add_child_model: function(model) {
38 38 // Called when a model is added to the children list.
39 39 var view = this.create_child_view(model);
40 40 this.$el.append(view.$el);
41 41
42 42 // Trigger the displayed event of the child view.
43 43 this.after_displayed(function() {
44 44 view.trigger('displayed');
45 45 });
46 46 },
47 47 });
48 48
49 49
50 50 var PopupView = widget.DOMWidgetView.extend({
51 51 render: function(){
52 52 // Called when view is rendered.
53 53 var that = this;
54 54
55 55 this.$el.on("remove", function(){
56 56 that.$backdrop.remove();
57 57 });
58 58 this.$backdrop = $('<div />')
59 59 .appendTo($('#notebook-container'))
60 60 .addClass('modal-dialog')
61 61 .css('position', 'absolute')
62 62 .css('left', '0px')
63 63 .css('top', '0px');
64 64 this.$window = $('<div />')
65 65 .appendTo(this.$backdrop)
66 66 .addClass('modal-content widget-modal')
67 67 .mousedown(function(){
68 68 that.bring_to_front();
69 69 });
70 70
71 71 // Set the elements array since the this.$window element is not child
72 72 // of this.$el and the parent widget manager or other widgets may
73 73 // need to know about all of the top-level widgets. The IPython
74 74 // widget manager uses this to register the elements with the
75 75 // keyboard manager.
76 76 this.additional_elements = [this.$window];
77 77
78 78 this.$title_bar = $('<div />')
79 79 .addClass('popover-title')
80 80 .appendTo(this.$window)
81 81 .mousedown(function(){
82 82 that.bring_to_front();
83 83 });
84 84 this.$close = $('<button />')
85 85 .addClass('close fa fa-remove')
86 86 .css('margin-left', '5px')
87 87 .appendTo(this.$title_bar)
88 88 .click(function(){
89 89 that.hide();
90 90 event.stopPropagation();
91 91 });
92 92 this.$minimize = $('<button />')
93 93 .addClass('close fa fa-arrow-down')
94 94 .appendTo(this.$title_bar)
95 95 .click(function(){
96 96 that.popped_out = !that.popped_out;
97 97 if (!that.popped_out) {
98 98 that.$minimize
99 99 .removeClass('fa fa-arrow-down')
100 100 .addClass('fa fa-arrow-up');
101 101
102 102 that.$window
103 103 .draggable('destroy')
104 104 .resizable('destroy')
105 105 .removeClass('widget-modal modal-content')
106 106 .addClass('docked-widget-modal')
107 107 .detach()
108 108 .insertBefore(that.$show_button);
109 109 that.$show_button.hide();
110 110 that.$close.hide();
111 111 } else {
112 112 that.$minimize
113 113 .addClass('fa fa-arrow-down')
114 114 .removeClass('fa fa-arrow-up');
115 115
116 116 that.$window
117 117 .removeClass('docked-widget-modal')
118 118 .addClass('widget-modal modal-content')
119 119 .detach()
120 120 .appendTo(that.$backdrop)
121 121 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
122 122 .resizable()
123 123 .children('.ui-resizable-handle').show();
124 124 that.show();
125 125 that.$show_button.show();
126 126 that.$close.show();
127 127 }
128 128 event.stopPropagation();
129 129 });
130 130 this.$title = $('<div />')
131 131 .addClass('widget-modal-title')
132 132 .html("&nbsp;")
133 133 .appendTo(this.$title_bar);
134 134 this.$body = $('<div />')
135 135 .addClass('modal-body')
136 136 .addClass('widget-modal-body')
137 137 .addClass('widget-container')
138 138 .addClass('vbox')
139 139 .appendTo(this.$window);
140 140
141 141 this.$show_button = $('<button />')
142 142 .html("&nbsp;")
143 143 .addClass('btn btn-info widget-modal-show')
144 144 .appendTo(this.$el)
145 145 .click(function(){
146 146 that.show();
147 147 });
148 148
149 149 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
150 150 this.$window.resizable();
151 151 this.$window.on('resize', function(){
152 152 that.$body.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
153 153 });
154 154
155 this.$el_to_style = this.$body;
156 155 this._shown_once = false;
157 156 this.popped_out = true;
158 157
159 158 this.update_children([], this.model.get('children'));
160 159 this.model.on('change:children', function(model, value) {
161 160 this.update_children(model.previous('children'), value);
162 161 }, this);
163 162 },
164 163
165 164 hide: function() {
166 165 // Called when the modal hide button is clicked.
167 166 this.$window.hide();
168 167 this.$show_button.removeClass('btn-info');
169 168 },
170 169
171 170 show: function() {
172 171 // Called when the modal show button is clicked.
173 172 this.$show_button.addClass('btn-info');
174 173 this.$window.show();
175 174 if (this.popped_out) {
176 175 this.$window.css("positon", "absolute");
177 176 this.$window.css("top", "0px");
178 177 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
179 178 $(window).scrollLeft()) + "px");
180 179 this.bring_to_front();
181 180 }
182 181 },
183 182
184 183 bring_to_front: function() {
185 184 // Make the modal top-most, z-ordered about the other modals.
186 185 var $widget_modals = $(".widget-modal");
187 186 var max_zindex = 0;
188 187 $widget_modals.each(function (index, el){
189 188 var zindex = parseInt($(el).css('z-index'));
190 189 if (!isNaN(zindex)) {
191 190 max_zindex = Math.max(max_zindex, zindex);
192 191 }
193 192 });
194 193
195 194 // Start z-index of widget modals at 2000
196 195 max_zindex = Math.max(max_zindex, 2000);
197 196
198 197 $widget_modals.each(function (index, el){
199 198 $el = $(el);
200 199 if (max_zindex == parseInt($el.css('z-index'))) {
201 200 $el.css('z-index', max_zindex - 1);
202 201 }
203 202 });
204 203 this.$window.css('z-index', max_zindex);
205 204 },
206 205
207 206 update_children: function(old_list, new_list) {
208 207 // Called when the children list is modified.
209 208 this.do_diff(old_list, new_list,
210 209 $.proxy(this.remove_child_model, this),
211 210 $.proxy(this.add_child_model, this));
212 211 },
213 212
214 213 remove_child_model: function(model) {
215 214 // Called when a child is removed from children list.
216 215 this.pop_child_view(model).remove();
217 216 },
218 217
219 218 add_child_model: function(model) {
220 219 // Called when a child is added to children list.
221 220 var view = this.create_child_view(model);
222 221 this.$body.append(view.$el);
223 222
224 223 // Trigger the displayed event of the child view.
225 224 this.after_displayed(function() {
226 225 view.trigger('displayed');
227 226 });
228 227 },
229 228
230 229 update: function(){
231 230 // Update the contents of this view
232 231 //
233 232 // Called when the model is changed. The model may have been
234 233 // changed by another view or by a state update from the back-end.
235 234 var description = this.model.get('description');
236 235 if (description.trim().length === 0) {
237 236 this.$title.html("&nbsp;"); // Preserve title height
238 237 } else {
239 238 this.$title.text(description);
240 239 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
241 240 }
242 241
243 242 var button_text = this.model.get('button_text');
244 243 if (button_text.trim().length === 0) {
245 244 this.$show_button.html("&nbsp;"); // Preserve button height
246 245 } else {
247 246 this.$show_button.text(button_text);
248 247 }
249 248
250 249 if (!this._shown_once) {
251 250 this._shown_once = true;
252 251 this.show();
253 252 }
254 253
255 254 return PopupView.__super__.update.apply(this);
256 255 },
257 256
258 257 _get_selector_element: function(selector) {
259 258 // Get an element view a 'special' jquery selector. (see widget.js)
260 259 //
261 260 // Since the modal actually isn't within the $el in the DOM, we need to extend
262 261 // the selector logic to allow the user to set css on the modal if need be.
263 262 // The convention used is:
264 263 // "modal" - select the modal div
265 264 // "modal [selector]" - select element(s) within the modal div.
266 265 // "[selector]" - select elements within $el
267 // "" - select the $el_to_style
266 // "" - select the $el
268 267 if (selector.substring(0, 5) == 'modal') {
269 268 if (selector == 'modal') {
270 269 return this.$window;
271 270 } else {
272 271 return this.$window.find(selector.substring(6));
273 272 }
274 273 } else {
275 274 return PopupView.__super__._get_selector_element.apply(this, [selector]);
276 275 }
277 276 },
278 277 });
279 278
280 279 return {
281 280 'ContainerView': ContainerView,
282 281 'PopupView': PopupView,
283 282 };
284 283 });
@@ -1,301 +1,298 b''
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-single');
15 15 this.$label = $('<div />')
16 16 .appendTo(this.$el)
17 17 .addClass('widget-hlabel')
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 this.$el_to_style = this.$slider_container; // Set default element to style
28 27 this.$el.append(this.$slider_container);
29 28
30 29 this.$readout = $('<div/>')
31 30 .appendTo(this.$el)
32 31 .addClass('widget-hreadout')
33 32 .hide();
34 33
35 34 // Set defaults.
36 35 this.update();
37 36 },
38 37
39 38 update : function(options){
40 39 // Update the contents of this view
41 40 //
42 41 // Called when the model is changed. The model may have been
43 42 // changed by another view or by a state update from the back-end.
44 43 if (options === undefined || options.updated_view != this) {
45 44 // JQuery slider option keys. These keys happen to have a
46 45 // one-to-one mapping with the corrosponding keys of the model.
47 46 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
48 47 var that = this;
49 48 that.$slider.slider({});
50 49 _.each(jquery_slider_keys, function(key, i) {
51 50 var model_value = that.model.get(key);
52 51 if (model_value !== undefined) {
53 52 that.$slider.slider("option", key, model_value);
54 53 }
55 54 });
56 55
57 56 // WORKAROUND FOR JQUERY SLIDER BUG.
58 57 // The horizontal position of the slider handle
59 58 // depends on the value of the slider at the time
60 59 // of orientation change. Before applying the new
61 60 // workaround, we set the value to the minimum to
62 61 // make sure that the horizontal placement of the
63 62 // handle in the vertical slider is always
64 63 // consistent.
65 64 var orientation = this.model.get('orientation');
66 65 var value = this.model.get('min');
67 66 this.$slider.slider('option', 'value', value);
68 67 this.$slider.slider('option', 'orientation', orientation);
69 68 value = this.model.get('value');
70 69 this.$slider.slider('option', 'value', value);
71 70 this.$readout.text(value);
72 71
73 72 // Use the right CSS classes for vertical & horizontal sliders
74 73 if (orientation=='vertical') {
75 74 this.$slider_container
76 75 .removeClass('widget-hslider')
77 76 .addClass('widget-vslider');
78 77 this.$el
79 78 .removeClass('widget-hbox-single')
80 79 .addClass('widget-vbox-single');
81 80 this.$label
82 81 .removeClass('widget-hlabel')
83 82 .addClass('widget-vlabel');
84 83 this.$readout
85 84 .removeClass('widget-hreadout')
86 85 .addClass('widget-vreadout');
87 86
88 87 } else {
89 88 this.$slider_container
90 89 .removeClass('widget-vslider')
91 90 .addClass('widget-hslider');
92 91 this.$el
93 92 .removeClass('widget-vbox-single')
94 93 .addClass('widget-hbox-single');
95 94 this.$label
96 95 .removeClass('widget-vlabel')
97 96 .addClass('widget-hlabel');
98 97 this.$readout
99 98 .removeClass('widget-vreadout')
100 99 .addClass('widget-hreadout');
101 100 }
102 101
103 102 var description = this.model.get('description');
104 103 if (description.length === 0) {
105 104 this.$label.hide();
106 105 } else {
107 106 this.$label.text(description);
108 107 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
109 108 this.$label.show();
110 109 }
111 110
112 111 var readout = this.model.get('readout');
113 112 if (readout) {
114 113 this.$readout.show();
115 114 } else {
116 115 this.$readout.hide();
117 116 }
118 117 }
119 118 return IntSliderView.__super__.update.apply(this);
120 119 },
121 120
122 121 events: {
123 122 // Dictionary of events and their handlers.
124 123 "slide" : "handleSliderChange"
125 124 },
126 125
127 126 handleSliderChange: function(e, ui) {
128 127 // Called when the slider value is changed.
129 128
130 129 // Calling model.set will trigger all of the other views of the
131 130 // model to update.
132 131 var actual_value = this._validate_slide_value(ui.value);
133 132 this.model.set('value', actual_value, {updated_view: this});
134 133 this.$readout.text(actual_value);
135 134 this.touch();
136 135 },
137 136
138 137 _validate_slide_value: function(x) {
139 138 // Validate the value of the slider before sending it to the back-end
140 139 // and applying it to the other views on the page.
141 140
142 141 // Double bit-wise not truncates the decimel (int cast).
143 142 return ~~x;
144 143 },
145 144 });
146 145
147 146
148 147 var IntTextView = widget.DOMWidgetView.extend({
149 148 render : function(){
150 149 // Called when view is rendered.
151 150 this.$el
152 151 .addClass('widget-hbox-single');
153 152 this.$label = $('<div />')
154 153 .appendTo(this.$el)
155 154 .addClass('widget-hlabel')
156 155 .hide();
157 156 this.$textbox = $('<input type="text" />')
158 157 .addClass('form-control')
159 158 .addClass('widget-numeric-text')
160 159 .appendTo(this.$el);
161 this.$el_to_style = this.$textbox; // Set default element to style
162 160 this.update(); // Set defaults.
163 161 },
164 162
165 163 update : function(options){
166 164 // Update the contents of this view
167 165 //
168 166 // Called when the model is changed. The model may have been
169 167 // changed by another view or by a state update from the back-end.
170 168 if (options === undefined || options.updated_view != this) {
171 169 var value = this.model.get('value');
172 170 if (this._parse_value(this.$textbox.val()) != value) {
173 171 this.$textbox.val(value);
174 172 }
175 173
176 174 if (this.model.get('disabled')) {
177 175 this.$textbox.attr('disabled','disabled');
178 176 } else {
179 177 this.$textbox.removeAttr('disabled');
180 178 }
181 179
182 180 var description = this.model.get('description');
183 181 if (description.length === 0) {
184 182 this.$label.hide();
185 183 } else {
186 184 this.$label.text(description);
187 185 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
188 186 this.$label.show();
189 187 }
190 188 }
191 189 return IntTextView.__super__.update.apply(this);
192 190 },
193 191
194 192 events: {
195 193 // Dictionary of events and their handlers.
196 194 "keyup input" : "handleChanging",
197 195 "paste input" : "handleChanging",
198 196 "cut input" : "handleChanging",
199 197
200 198 // Fires only when control is validated or looses focus.
201 199 "change input" : "handleChanged"
202 200 },
203 201
204 202 handleChanging: function(e) {
205 203 // Handles and validates user input.
206 204
207 205 // Try to parse value as a int.
208 206 var numericalValue = 0;
209 207 if (e.target.value !== '') {
210 208 var trimmed = e.target.value.trim();
211 209 if (!(['-', '-.', '.', '+.', '+'].indexOf(trimmed) >= 0)) {
212 210 numericalValue = this._parse_value(e.target.value);
213 211 }
214 212 }
215 213
216 214 // If parse failed, reset value to value stored in model.
217 215 if (isNaN(numericalValue)) {
218 216 e.target.value = this.model.get('value');
219 217 } else if (!isNaN(numericalValue)) {
220 218 if (this.model.get('max') !== undefined) {
221 219 numericalValue = Math.min(this.model.get('max'), numericalValue);
222 220 }
223 221 if (this.model.get('min') !== undefined) {
224 222 numericalValue = Math.max(this.model.get('min'), numericalValue);
225 223 }
226 224
227 225 // Apply the value if it has changed.
228 226 if (numericalValue != this.model.get('value')) {
229 227
230 228 // Calling model.set will trigger all of the other views of the
231 229 // model to update.
232 230 this.model.set('value', numericalValue, {updated_view: this});
233 231 this.touch();
234 232 }
235 233 }
236 234 },
237 235
238 236 handleChanged: function(e) {
239 237 // Applies validated input.
240 238 if (this.model.get('value') != e.target.value) {
241 239 e.target.value = this.model.get('value');
242 240 }
243 241 },
244 242
245 243 _parse_value: function(value) {
246 244 // Parse the value stored in a string.
247 245 return parseInt(value);
248 246 },
249 247 });
250 248
251 249
252 250 var ProgressView = widget.DOMWidgetView.extend({
253 251 render : function(){
254 252 // Called when view is rendered.
255 253 this.$el
256 254 .addClass('widget-hbox-single');
257 255 this.$label = $('<div />')
258 256 .appendTo(this.$el)
259 257 .addClass('widget-hlabel')
260 258 .hide();
261 259 this.$progress = $('<div />')
262 260 .addClass('progress')
263 261 .addClass('widget-progress')
264 262 .appendTo(this.$el);
265 this.$el_to_style = this.$progress; // Set default element to style
266 263 this.$bar = $('<div />')
267 264 .addClass('progress-bar')
268 265 .css('width', '50%')
269 266 .appendTo(this.$progress);
270 267 this.update(); // Set defaults.
271 268 },
272 269
273 270 update : function(){
274 271 // Update the contents of this view
275 272 //
276 273 // Called when the model is changed. The model may have been
277 274 // changed by another view or by a state update from the back-end.
278 275 var value = this.model.get('value');
279 276 var max = this.model.get('max');
280 277 var min = this.model.get('min');
281 278 var percent = 100.0 * (value - min) / (max - min);
282 279 this.$bar.css('width', percent + '%');
283 280
284 281 var description = this.model.get('description');
285 282 if (description.length === 0) {
286 283 this.$label.hide();
287 284 } else {
288 285 this.$label.text(description);
289 286 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
290 287 this.$label.show();
291 288 }
292 289 return ProgressView.__super__.update.apply(this);
293 290 },
294 291 });
295 292
296 293 return {
297 294 'IntSliderView': IntSliderView,
298 295 'IntTextView': IntTextView,
299 296 'ProgressView': ProgressView,
300 297 };
301 298 });
@@ -1,380 +1,376 b''
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 "base/js/utils",
7 7 "jquery",
8 8 "bootstrap",
9 9 ], function(widget, utils, $){
10 10
11 11 var DropdownView = widget.DOMWidgetView.extend({
12 12 render : function(){
13 13 // Called when view is rendered.
14 14 this.$el
15 15 .addClass('widget-hbox-single');
16 16 this.$label = $('<div />')
17 17 .appendTo(this.$el)
18 18 .addClass('widget-hlabel')
19 19 .hide();
20 20 this.$buttongroup = $('<div />')
21 21 .addClass('widget_item')
22 22 .addClass('btn-group')
23 23 .appendTo(this.$el);
24 this.$el_to_style = this.$buttongroup; // Set default element to style
25 24 this.$droplabel = $('<button />')
26 25 .addClass('btn btn-default')
27 26 .addClass('widget-combo-btn')
28 27 .html("&nbsp;")
29 28 .appendTo(this.$buttongroup);
30 29 this.$dropbutton = $('<button />')
31 30 .addClass('btn btn-default')
32 31 .addClass('dropdown-toggle')
33 32 .addClass('widget-combo-carrot-btn')
34 33 .attr('data-toggle', 'dropdown')
35 34 .append($('<span />').addClass("caret"))
36 35 .appendTo(this.$buttongroup);
37 36 this.$droplist = $('<ul />')
38 37 .addClass('dropdown-menu')
39 38 .appendTo(this.$buttongroup);
40 39
41 40 // Set defaults.
42 41 this.update();
43 42 },
44 43
45 44 update : function(options){
46 45 // Update the contents of this view
47 46 //
48 47 // Called when the model is changed. The model may have been
49 48 // changed by another view or by a state update from the back-end.
50 49
51 50 if (options === undefined || options.updated_view != this) {
52 51 var selected_item_text = this.model.get('value_name');
53 52 if (selected_item_text.trim().length === 0) {
54 53 this.$droplabel.html("&nbsp;");
55 54 } else {
56 55 this.$droplabel.text(selected_item_text);
57 56 }
58 57
59 58 var items = this.model.get('value_names');
60 59 var $replace_droplist = $('<ul />')
61 60 .addClass('dropdown-menu');
62 61 var that = this;
63 62 _.each(items, function(item, i) {
64 63 var item_button = $('<a href="#"/>')
65 64 .text(item)
66 65 .on('click', $.proxy(that.handle_click, that));
67 66 $replace_droplist.append($('<li />').append(item_button));
68 67 });
69 68
70 69 this.$droplist.replaceWith($replace_droplist);
71 70 this.$droplist.remove();
72 71 this.$droplist = $replace_droplist;
73 72
74 73 if (this.model.get('disabled')) {
75 74 this.$buttongroup.attr('disabled','disabled');
76 75 this.$droplabel.attr('disabled','disabled');
77 76 this.$dropbutton.attr('disabled','disabled');
78 77 this.$droplist.attr('disabled','disabled');
79 78 } else {
80 79 this.$buttongroup.removeAttr('disabled');
81 80 this.$droplabel.removeAttr('disabled');
82 81 this.$dropbutton.removeAttr('disabled');
83 82 this.$droplist.removeAttr('disabled');
84 83 }
85 84
86 85 var description = this.model.get('description');
87 86 if (description.length === 0) {
88 87 this.$label.hide();
89 88 } else {
90 89 this.$label.text(description);
91 90 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
92 91 this.$label.show();
93 92 }
94 93 }
95 94 return DropdownView.__super__.update.apply(this);
96 95 },
97 96
98 97 handle_click: function (e) {
99 98 // Handle when a value is clicked.
100 99
101 100 // Calling model.set will trigger all of the other views of the
102 101 // model to update.
103 102 this.model.set('value_name', $(e.target).text(), {updated_view: this});
104 103 this.touch();
105 104 },
106 105
107 106 });
108 107
109 108
110 109 var RadioButtonsView = widget.DOMWidgetView.extend({
111 110 render : function(){
112 111 // Called when view is rendered.
113 112 this.$el
114 113 .addClass('widget-hbox');
115 114 this.$label = $('<div />')
116 115 .appendTo(this.$el)
117 116 .addClass('widget-hlabel')
118 117 .hide();
119 118 this.$container = $('<div />')
120 119 .appendTo(this.$el)
121 120 .addClass('widget-radio-box');
122 this.$el_to_style = this.$container; // Set default element to style
123 121 this.update();
124 122 },
125 123
126 124 update : function(options){
127 125 // Update the contents of this view
128 126 //
129 127 // Called when the model is changed. The model may have been
130 128 // changed by another view or by a state update from the back-end.
131 129 if (options === undefined || options.updated_view != this) {
132 130 // Add missing items to the DOM.
133 131 var items = this.model.get('value_names');
134 132 var disabled = this.model.get('disabled');
135 133 var that = this;
136 134 _.each(items, function(item, index) {
137 135 var item_query = ' :input[value="' + item + '"]';
138 136 if (that.$el.find(item_query).length === 0) {
139 137 var $label = $('<label />')
140 138 .addClass('radio')
141 139 .text(item)
142 140 .appendTo(that.$container);
143 141
144 142 $('<input />')
145 143 .attr('type', 'radio')
146 144 .addClass(that.model)
147 145 .val(item)
148 146 .prependTo($label)
149 147 .on('click', $.proxy(that.handle_click, that));
150 148 }
151 149
152 150 var $item_element = that.$container.find(item_query);
153 151 if (that.model.get('value_name') == item) {
154 152 $item_element.prop('checked', true);
155 153 } else {
156 154 $item_element.prop('checked', false);
157 155 }
158 156 $item_element.prop('disabled', disabled);
159 157 });
160 158
161 159 // Remove items that no longer exist.
162 160 this.$container.find('input').each(function(i, obj) {
163 161 var value = $(obj).val();
164 162 var found = false;
165 163 _.each(items, function(item, index) {
166 164 if (item == value) {
167 165 found = true;
168 166 return false;
169 167 }
170 168 });
171 169
172 170 if (!found) {
173 171 $(obj).parent().remove();
174 172 }
175 173 });
176 174
177 175 var description = this.model.get('description');
178 176 if (description.length === 0) {
179 177 this.$label.hide();
180 178 } else {
181 179 this.$label.text(description);
182 180 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
183 181 this.$label.show();
184 182 }
185 183 }
186 184 return RadioButtonsView.__super__.update.apply(this);
187 185 },
188 186
189 187 handle_click: function (e) {
190 188 // Handle when a value is clicked.
191 189
192 190 // Calling model.set will trigger all of the other views of the
193 191 // model to update.
194 192 this.model.set('value_name', $(e.target).val(), {updated_view: this});
195 193 this.touch();
196 194 },
197 195 });
198 196
199 197
200 198 var ToggleButtonsView = widget.DOMWidgetView.extend({
201 199 render : function(){
202 200 // Called when view is rendered.
203 201 this.$el
204 202 .addClass('widget-hbox-single');
205 203 this.$label = $('<div />')
206 204 .appendTo(this.$el)
207 205 .addClass('widget-hlabel')
208 206 .hide();
209 207 this.$buttongroup = $('<div />')
210 208 .addClass('btn-group')
211 209 .attr('data-toggle', 'buttons-radio')
212 210 .appendTo(this.$el);
213 this.$el_to_style = this.$buttongroup; // Set default element to style
214 211 this.update();
215 212 },
216 213
217 214 update : function(options){
218 215 // Update the contents of this view
219 216 //
220 217 // Called when the model is changed. The model may have been
221 218 // changed by another view or by a state update from the back-end.
222 219 if (options === undefined || options.updated_view != this) {
223 220 // Add missing items to the DOM.
224 221 var items = this.model.get('value_names');
225 222 var disabled = this.model.get('disabled');
226 223 var that = this;
227 224 var item_html;
228 225 _.each(items, function(item, index) {
229 226 if (item.trim().length == 0) {
230 227 item_html = "&nbsp;";
231 228 } else {
232 229 item_html = utils.escape_html(item);
233 230 }
234 231 var item_query = '[data-value="' + item + '"]';
235 232 var $item_element = that.$buttongroup.find(item_query);
236 233 if (!$item_element.length) {
237 234 $item_element = $('<button/>')
238 235 .attr('type', 'button')
239 236 .addClass('btn btn-default')
240 237 .html(item_html)
241 238 .appendTo(that.$buttongroup)
242 239 .attr('data-value', item)
243 240 .on('click', $.proxy(that.handle_click, that));
244 241 }
245 242 if (that.model.get('value_name') == item) {
246 243 $item_element.addClass('active');
247 244 } else {
248 245 $item_element.removeClass('active');
249 246 }
250 247 $item_element.prop('disabled', disabled);
251 248 });
252 249
253 250 // Remove items that no longer exist.
254 251 this.$buttongroup.find('button').each(function(i, obj) {
255 252 var value = $(obj).data('value');
256 253 var found = false;
257 254 _.each(items, function(item, index) {
258 255 if (item == value) {
259 256 found = true;
260 257 return false;
261 258 }
262 259 });
263 260
264 261 if (!found) {
265 262 $(obj).remove();
266 263 }
267 264 });
268 265
269 266 var description = this.model.get('description');
270 267 if (description.length === 0) {
271 268 this.$label.hide();
272 269 } else {
273 270 this.$label.text(description);
274 271 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
275 272 this.$label.show();
276 273 }
277 274 }
278 275 return ToggleButtonsView.__super__.update.apply(this);
279 276 },
280 277
281 278 handle_click: function (e) {
282 279 // Handle when a value is clicked.
283 280
284 281 // Calling model.set will trigger all of the other views of the
285 282 // model to update.
286 283 this.model.set('value_name', $(e.target).data('value'), {updated_view: this});
287 284 this.touch();
288 285 },
289 286 });
290 287
291 288
292 289 var SelectView = widget.DOMWidgetView.extend({
293 290 render : function(){
294 291 // Called when view is rendered.
295 292 this.$el
296 293 .addClass('widget-hbox');
297 294 this.$label = $('<div />')
298 295 .appendTo(this.$el)
299 296 .addClass('widget-hlabel')
300 297 .hide();
301 298 this.$listbox = $('<select />')
302 299 .addClass('widget-listbox form-control')
303 300 .attr('size', 6)
304 301 .appendTo(this.$el);
305 this.$el_to_style = this.$listbox; // Set default element to style
306 302 this.update();
307 303 },
308 304
309 305 update : function(options){
310 306 // Update the contents of this view
311 307 //
312 308 // Called when the model is changed. The model may have been
313 309 // changed by another view or by a state update from the back-end.
314 310 if (options === undefined || options.updated_view != this) {
315 311 // Add missing items to the DOM.
316 312 var items = this.model.get('value_names');
317 313 var that = this;
318 314 _.each(items, function(item, index) {
319 315 var item_query = ' :contains("' + item + '")';
320 316 if (that.$listbox.find(item_query).length === 0) {
321 317 $('<option />')
322 318 .text(item)
323 319 .attr('value_name', item)
324 320 .appendTo(that.$listbox)
325 321 .on('click', $.proxy(that.handle_click, that));
326 322 }
327 323 });
328 324
329 325 // Select the correct element
330 326 this.$listbox.val(this.model.get('value_name'));
331 327
332 328 // Disable listbox if needed
333 329 var disabled = this.model.get('disabled');
334 330 this.$listbox.prop('disabled', disabled);
335 331
336 332 // Remove items that no longer exist.
337 333 this.$listbox.find('option').each(function(i, obj) {
338 334 var value = $(obj).text();
339 335 var found = false;
340 336 _.each(items, function(item, index) {
341 337 if (item == value) {
342 338 found = true;
343 339 return false;
344 340 }
345 341 });
346 342
347 343 if (!found) {
348 344 $(obj).remove();
349 345 }
350 346 });
351 347
352 348 var description = this.model.get('description');
353 349 if (description.length === 0) {
354 350 this.$label.hide();
355 351 } else {
356 352 this.$label.text(description);
357 353 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
358 354 this.$label.show();
359 355 }
360 356 }
361 357 return SelectView.__super__.update.apply(this);
362 358 },
363 359
364 360 handle_click: function (e) {
365 361 // Handle when a value is clicked.
366 362
367 363 // Calling model.set will trigger all of the other views of the
368 364 // model to update.
369 365 this.model.set('value_name', $(e.target).text(), {updated_view: this});
370 366 this.touch();
371 367 },
372 368 });
373 369
374 370 return {
375 371 'DropdownView': DropdownView,
376 372 'RadioButtonsView': RadioButtonsView,
377 373 'ToggleButtonsView': ToggleButtonsView,
378 374 'SelectView': SelectView,
379 375 };
380 376 });
@@ -1,242 +1,240 b''
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 "jquery",
7 7 "bootstrap",
8 8 ], function(widget, $){
9 9
10 10 var HTMLView = widget.DOMWidgetView.extend({
11 11 render : function(){
12 12 // Called when view is rendered.
13 13 this.update(); // Set defaults.
14 14 },
15 15
16 16 update : function(){
17 17 // Update the contents of this view
18 18 //
19 19 // Called when the model is changed. The model may have been
20 20 // changed by another view or by a state update from the back-end.
21 21 this.$el.html(this.model.get('value')); // CAUTION! .html(...) CALL MANDITORY!!!
22 22 return HTMLView.__super__.update.apply(this);
23 23 },
24 24 });
25 25
26 26
27 27 var LatexView = widget.DOMWidgetView.extend({
28 28 render : function(){
29 29 // Called when view is rendered.
30 30 this.update(); // Set defaults.
31 31 },
32 32
33 33 update : function(){
34 34 // Update the contents of this view
35 35 //
36 36 // Called when the model is changed. The model may have been
37 37 // changed by another view or by a state update from the back-end.
38 38 this.$el.text(this.model.get('value'));
39 39 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$el.get(0)]);
40 40
41 41 return LatexView.__super__.update.apply(this);
42 42 },
43 43 });
44 44
45 45
46 46 var TextareaView = widget.DOMWidgetView.extend({
47 47 render: function(){
48 48 // Called when view is rendered.
49 49 this.$el
50 50 .addClass('widget-hbox');
51 51 this.$label = $('<div />')
52 52 .appendTo(this.$el)
53 53 .addClass('widget-hlabel')
54 54 .hide();
55 55 this.$textbox = $('<textarea />')
56 56 .attr('rows', 5)
57 57 .addClass('widget-text form-control')
58 58 .appendTo(this.$el);
59 this.$el_to_style = this.$textbox; // Set default element to style
60 59 this.update(); // Set defaults.
61 60
62 61 this.model.on('msg:custom', $.proxy(this._handle_textarea_msg, this));
63 62 this.model.on('change:placeholder', function(model, value, options) {
64 63 this.update_placeholder(value);
65 64 }, this);
66 65
67 66 this.update_placeholder();
68 67 },
69 68
70 69 _handle_textarea_msg: function (content){
71 70 // Handle when a custom msg is recieved from the back-end.
72 71 if (content.method == "scroll_to_bottom") {
73 72 this.scroll_to_bottom();
74 73 }
75 74 },
76 75
77 76 update_placeholder: function(value) {
78 77 if (!value) {
79 78 value = this.model.get('placeholder');
80 79 }
81 80 this.$textbox.attr('placeholder', value);
82 81 },
83 82
84 83 scroll_to_bottom: function (){
85 84 // Scroll the text-area view to the bottom.
86 85 this.$textbox.scrollTop(this.$textbox[0].scrollHeight);
87 86 },
88 87
89 88 update: function(options){
90 89 // Update the contents of this view
91 90 //
92 91 // Called when the model is changed. The model may have been
93 92 // changed by another view or by a state update from the back-end.
94 93 if (options === undefined || options.updated_view != this) {
95 94 this.$textbox.val(this.model.get('value'));
96 95
97 96 var disabled = this.model.get('disabled');
98 97 this.$textbox.prop('disabled', disabled);
99 98
100 99 var description = this.model.get('description');
101 100 if (description.length === 0) {
102 101 this.$label.hide();
103 102 } else {
104 103 this.$label.text(description);
105 104 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
106 105 this.$label.show();
107 106 }
108 107 }
109 108 return TextareaView.__super__.update.apply(this);
110 109 },
111 110
112 111 events: {
113 112 // Dictionary of events and their handlers.
114 113 "keyup textarea" : "handleChanging",
115 114 "paste textarea" : "handleChanging",
116 115 "cut textarea" : "handleChanging"
117 116 },
118 117
119 118 handleChanging: function(e) {
120 119 // Handles and validates user input.
121 120
122 121 // Calling model.set will trigger all of the other views of the
123 122 // model to update.
124 123 this.model.set('value', e.target.value, {updated_view: this});
125 124 this.touch();
126 125 },
127 126 });
128 127
129 128
130 129 var TextView = widget.DOMWidgetView.extend({
131 130 render: function(){
132 131 // Called when view is rendered.
133 132 this.$el
134 133 .addClass('widget-hbox-single');
135 134 this.$label = $('<div />')
136 135 .addClass('widget-hlabel')
137 136 .appendTo(this.$el)
138 137 .hide();
139 138 this.$textbox = $('<input type="text" />')
140 139 .addClass('input')
141 140 .addClass('widget-text form-control')
142 141 .appendTo(this.$el);
143 this.$el_to_style = this.$textbox; // Set default element to style
144 142 this.update(); // Set defaults.
145 143 this.model.on('change:placeholder', function(model, value, options) {
146 144 this.update_placeholder(value);
147 145 }, this);
148 146
149 147 this.update_placeholder();
150 148 },
151 149
152 150 update_placeholder: function(value) {
153 151 if (!value) {
154 152 value = this.model.get('placeholder');
155 153 }
156 154 this.$textbox.attr('placeholder', value);
157 155 },
158 156
159 157 update: function(options){
160 158 // Update the contents of this view
161 159 //
162 160 // Called when the model is changed. The model may have been
163 161 // changed by another view or by a state update from the back-end.
164 162 if (options === undefined || options.updated_view != this) {
165 163 if (this.$textbox.val() != this.model.get('value')) {
166 164 this.$textbox.val(this.model.get('value'));
167 165 }
168 166
169 167 var disabled = this.model.get('disabled');
170 168 this.$textbox.prop('disabled', disabled);
171 169
172 170 var description = this.model.get('description');
173 171 if (description.length === 0) {
174 172 this.$label.hide();
175 173 } else {
176 174 this.$label.text(description);
177 175 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
178 176 this.$label.show();
179 177 }
180 178 }
181 179 return TextView.__super__.update.apply(this);
182 180 },
183 181
184 182 events: {
185 183 // Dictionary of events and their handlers.
186 184 "keyup input" : "handleChanging",
187 185 "paste input" : "handleChanging",
188 186 "cut input" : "handleChanging",
189 187 "keypress input" : "handleKeypress",
190 188 "blur input" : "handleBlur",
191 189 "focusout input" : "handleFocusOut"
192 190 },
193 191
194 192 handleChanging: function(e) {
195 193 // Handles user input.
196 194
197 195 // Calling model.set will trigger all of the other views of the
198 196 // model to update.
199 197 this.model.set('value', e.target.value, {updated_view: this});
200 198 this.touch();
201 199 },
202 200
203 201 handleKeypress: function(e) {
204 202 // Handles text submition
205 203 if (e.keyCode == 13) { // Return key
206 204 this.send({event: 'submit'});
207 205 event.stopPropagation();
208 206 event.preventDefault();
209 207 return false;
210 208 }
211 209 },
212 210
213 211 handleBlur: function(e) {
214 212 // Prevent a blur from firing if the blur was not user intended.
215 213 // This is a workaround for the return-key focus loss bug.
216 214 // TODO: Is the original bug actually a fault of the keyboard
217 215 // manager?
218 216 if (e.relatedTarget === null) {
219 217 event.stopPropagation();
220 218 event.preventDefault();
221 219 return false;
222 220 }
223 221 },
224 222
225 223 handleFocusOut: function(e) {
226 224 // Prevent a blur from firing if the blur was not user intended.
227 225 // This is a workaround for the return-key focus loss bug.
228 226 if (e.relatedTarget === null) {
229 227 event.stopPropagation();
230 228 event.preventDefault();
231 229 return false;
232 230 }
233 231 },
234 232 });
235 233
236 234 return {
237 235 'HTMLView': HTMLView,
238 236 'LatexView': LatexView,
239 237 'TextareaView': TextareaView,
240 238 'TextView': TextView,
241 239 };
242 240 });
@@ -1,188 +1,188 b''
1 1 var xor = function (a, b) {return !a ^ !b;};
2 2 var isArray = function (a) {
3 3 try {
4 4 return Object.toString.call(a) === "[object Array]" || Object.toString.call(a) === "[object RuntimeArray]";
5 5 } catch (e) {
6 6 return Array.isArray(a);
7 7 }
8 8 };
9 9 var recursive_compare = function(a, b) {
10 10 // Recursively compare two objects.
11 11 var same = true;
12 12 same = same && !xor(a instanceof Object || typeof a == 'object', b instanceof Object || typeof b == 'object');
13 13 same = same && !xor(isArray(a), isArray(b));
14 14
15 15 if (same) {
16 16 if (a instanceof Object) {
17 17 var key;
18 18 for (key in a) {
19 19 if (a.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
20 20 same = false;
21 21 break;
22 22 }
23 23 }
24 24 for (key in b) {
25 25 if (b.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
26 26 same = false;
27 27 break;
28 28 }
29 29 }
30 30 } else {
31 31 return a === b;
32 32 }
33 33 }
34 34
35 35 return same;
36 36 };
37 37
38 38 // Test the widget framework.
39 39 casper.notebook_test(function () {
40 40 var index;
41 41
42 42 this.then(function () {
43 43
44 44 // Check if the WidgetManager class is defined.
45 45 this.test.assert(this.evaluate(function() {
46 46 return IPython.WidgetManager !== undefined;
47 47 }), 'WidgetManager class is defined');
48 48 });
49 49
50 50 index = this.append_cell(
51 51 'from IPython.html import widgets\n' +
52 52 'from IPython.display import display, clear_output\n' +
53 53 'print("Success")');
54 54 this.execute_cell_then(index);
55 55
56 56 this.then(function () {
57 57 // Check if the widget manager has been instantiated.
58 58 this.test.assert(this.evaluate(function() {
59 59 return IPython.notebook.kernel.widget_manager !== undefined;
60 60 }), 'Notebook widget manager instantiated');
61 61
62 62 // Functions that can be used to test the packing and unpacking APIs
63 63 var that = this;
64 64 var test_pack = function (input) {
65 65 var output = that.evaluate(function(input) {
66 66 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
67 67 var results = model._pack_models(input);
68 68 return results;
69 69 }, {input: input});
70 70 that.test.assert(recursive_compare(input, output),
71 71 JSON.stringify(input) + ' passed through Model._pack_model unchanged');
72 72 };
73 73 var test_unpack = function (input) {
74 74 var output = that.evaluate(function(input) {
75 75 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
76 76 var results = model._unpack_models(input);
77 77 return results;
78 78 }, {input: input});
79 79 that.test.assert(recursive_compare(input, output),
80 80 JSON.stringify(input) + ' passed through Model._unpack_model unchanged');
81 81 };
82 82 var test_packing = function(input) {
83 83 test_pack(input);
84 84 test_unpack(input);
85 85 };
86 86
87 87 test_packing({0: 'hi', 1: 'bye'});
88 88 test_packing(['hi', 'bye']);
89 89 test_packing(['hi', 5]);
90 90 test_packing(['hi', '5']);
91 91 test_packing([1.0, 0]);
92 92 test_packing([1.0, false]);
93 93 test_packing([1, false]);
94 94 test_packing([1, false, {a: 'hi'}]);
95 95 test_packing([1, false, ['hi']]);
96 96
97 97 // Test multi-set, single touch code. First create a custom widget.
98 98 this.evaluate(function() {
99 99 var MultiSetView = IPython.DOMWidgetView.extend({
100 100 render: function(){
101 101 this.model.set('a', 1);
102 102 this.model.set('b', 2);
103 103 this.model.set('c', 3);
104 104 this.touch();
105 105 },
106 106 });
107 107 IPython.WidgetManager.register_widget_view('MultiSetView', MultiSetView);
108 108 }, {});
109 109 });
110 110
111 111 // Try creating the multiset widget, verify that sets the values correctly.
112 112 var multiset = {};
113 113 multiset.index = this.append_cell(
114 114 'from IPython.utils.traitlets import Unicode, CInt\n' +
115 115 'class MultiSetWidget(widgets.Widget):\n' +
116 116 ' _view_name = Unicode("MultiSetView", sync=True)\n' +
117 117 ' a = CInt(0, sync=True)\n' +
118 118 ' b = CInt(0, sync=True)\n' +
119 119 ' c = CInt(0, sync=True)\n' +
120 120 ' d = CInt(-1, sync=True)\n' + // See if it sends a full state.
121 121 ' def _handle_receive_state(self, sync_data):\n' +
122 122 ' widgets.Widget._handle_receive_state(self, sync_data)\n'+
123 123 ' self.d = len(sync_data)\n' +
124 124 'multiset = MultiSetWidget()\n' +
125 125 'display(multiset)\n' +
126 126 'print(multiset.model_id)');
127 127 this.execute_cell_then(multiset.index, function(index) {
128 128 multiset.model_id = this.get_output_cell(index).text.trim();
129 129 });
130 130
131 131 this.wait_for_widget(multiset);
132 132
133 133 index = this.append_cell(
134 134 'print("%d%d%d" % (multiset.a, multiset.b, multiset.c))');
135 135 this.execute_cell_then(index, function(index) {
136 136 this.test.assertEquals(this.get_output_cell(index).text.trim(), '123',
137 137 'Multiple model.set calls and one view.touch update state in back-end.');
138 138 });
139 139
140 140 index = this.append_cell(
141 141 'print("%d" % (multiset.d))');
142 142 this.execute_cell_then(index, function(index) {
143 143 this.test.assertEquals(this.get_output_cell(index).text.trim(), '3',
144 144 'Multiple model.set calls sent a partial state.');
145 145 });
146 146
147 147 var textbox = {};
148 148 throttle_index = this.append_cell(
149 149 'import time\n' +
150 150 'textbox = widgets.TextWidget()\n' +
151 151 'display(textbox)\n' +
152 'textbox.add_class("my-throttle-textbox")\n' +
152 'textbox.add_class("my-throttle-textbox", selector="input")\n' +
153 153 'def handle_change(name, old, new):\n' +
154 154 ' display(len(new))\n' +
155 155 ' time.sleep(0.5)\n' +
156 156 'textbox.on_trait_change(handle_change, "value")\n' +
157 157 'print(textbox.model_id)');
158 158 this.execute_cell_then(throttle_index, function(index){
159 159 textbox.model_id = this.get_output_cell(index).text.trim();
160 160
161 161 this.test.assert(this.cell_element_exists(index,
162 162 '.widget-area .widget-subarea'),
163 163 'Widget subarea exists.');
164 164
165 165 this.test.assert(this.cell_element_exists(index,
166 166 '.my-throttle-textbox'), 'Textbox exists.');
167 167
168 168 // Send 20 characters
169 169 this.sendKeys('.my-throttle-textbox', '....................');
170 170 });
171 171
172 172 this.wait_for_widget(textbox);
173 173
174 174 this.then(function () {
175 175 var outputs = this.evaluate(function(i) {
176 176 return IPython.notebook.get_cell(i).output_area.outputs;
177 177 }, {i : throttle_index});
178 178
179 179 // Only 4 outputs should have printed, but because of timing, sometimes
180 180 // 5 outputs will print. All we need to do is verify num outputs <= 5
181 181 // because that is much less than 20.
182 182 this.test.assert(outputs.length <= 5, 'Messages throttled.');
183 183
184 184 // We also need to verify that the last state sent was correct.
185 185 var last_state = outputs[outputs.length-1]['text/plain'];
186 186 this.test.assertEquals(last_state, "20", "Last state sent when throttling.");
187 187 });
188 188 });
@@ -1,100 +1,100 b''
1 1 // Test widget float class
2 2 casper.notebook_test(function () {
3 3 index = this.append_cell(
4 4 'from IPython.html import widgets\n' +
5 5 'from IPython.display import display, clear_output\n' +
6 6 'print("Success")');
7 7 this.execute_cell_then(index);
8 8
9 9 var float_text = {};
10 10 float_text.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-float-text';
11 11 float_text.index = this.append_cell(
12 12 'float_widget = widgets.FloatTextWidget()\n' +
13 13 'display(float_widget)\n' +
14 'float_widget.add_class("my-second-float-text")\n' +
14 'float_widget.add_class("my-second-float-text", selector="input")\n' +
15 15 'print(float_widget.model_id)\n');
16 16 this.execute_cell_then(float_text.index, function(index){
17 17 float_text.model_id = this.get_output_cell(index).text.trim();
18 18
19 19 this.test.assert(this.cell_element_exists(index,
20 20 '.widget-area .widget-subarea'),
21 21 'Widget subarea exists.');
22 22
23 23 this.test.assert(this.cell_element_exists(index, float_text.query),
24 24 'Widget float textbox exists.');
25 25
26 26 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
27 27 this.sendKeys(float_text.query, '1.05');
28 28 });
29 29
30 30 this.wait_for_widget(float_text);
31 31
32 32 index = this.append_cell('print(float_widget.value)\n');
33 33 this.execute_cell_then(index, function(index){
34 34 this.test.assertEquals(this.get_output_cell(index).text, '1.05\n',
35 35 'Float textbox value set.');
36 36 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
37 37 this.sendKeys(float_text.query, '123456789.0');
38 38 });
39 39
40 40 this.wait_for_widget(float_text);
41 41
42 42 index = this.append_cell('print(float_widget.value)\n');
43 43 this.execute_cell_then(index, function(index){
44 44 this.test.assertEquals(this.get_output_cell(index).text, '123456789.0\n',
45 45 'Long float textbox value set (probably triggers throttling).');
46 46 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
47 47 this.sendKeys(float_text.query, '12hello');
48 48 });
49 49
50 50 this.wait_for_widget(float_text);
51 51
52 52 index = this.append_cell('print(float_widget.value)\n');
53 53 this.execute_cell_then(index, function(index){
54 54 this.test.assertEquals(this.get_output_cell(index).text, '12.0\n',
55 55 'Invald float textbox value caught and filtered.');
56 56 });
57 57
58 58 var float_text_query = '.widget-area .widget-subarea .widget-hbox-single .widget-numeric-text';
59 59 var slider = {};
60 60 slider.query = '.widget-area .widget-subarea .widget-hbox-single .slider';
61 61 slider.index = this.append_cell(
62 62 'floatrange = [widgets.BoundedFloatTextWidget(), \n' +
63 63 ' widgets.FloatSliderWidget()]\n' +
64 64 '[display(floatrange[i]) for i in range(2)]\n' +
65 65 'print("Success")\n');
66 66 this.execute_cell_then(slider.index, function(index){
67 67
68 68 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
69 69 'Create float range cell executed with correct output.');
70 70
71 71 this.test.assert(this.cell_element_exists(index,
72 72 '.widget-area .widget-subarea'),
73 73 'Widget subarea exists.');
74 74
75 75 this.test.assert(this.cell_element_exists(index, slider.query),
76 76 'Widget slider exists.');
77 77
78 78 this.test.assert(this.cell_element_exists(index, float_text_query),
79 79 'Widget float textbox exists.');
80 80 });
81 81
82 82 index = this.append_cell(
83 83 'for widget in floatrange:\n' +
84 84 ' widget.max = 50.0\n' +
85 85 ' widget.min = -50.0\n' +
86 86 ' widget.value = 25.0\n' +
87 87 'print("Success")\n');
88 88 this.execute_cell_then(index, function(index){
89 89
90 90 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
91 91 'Float range properties cell executed with correct output.');
92 92
93 93 this.test.assert(this.cell_element_exists(slider.index, slider.query),
94 94 'Widget slider exists.');
95 95
96 96 this.test.assert(this.cell_element_function(slider.index, slider.query,
97 97 'slider', ['value']) == 25.0,
98 98 'Slider set to Python value.');
99 99 });
100 100 }); No newline at end of file
@@ -1,157 +1,157 b''
1 1 // Test widget int class
2 2 casper.notebook_test(function () {
3 3 index = this.append_cell(
4 4 'from IPython.html import widgets\n' +
5 5 'from IPython.display import display, clear_output\n' +
6 6 'print("Success")');
7 7 this.execute_cell_then(index);
8 8
9 var int_text = {}
9 var int_text = {};
10 10 int_text.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-int-text';
11 11 int_text.index = this.append_cell(
12 12 'int_widget = widgets.IntTextWidget()\n' +
13 13 'display(int_widget)\n' +
14 'int_widget.add_class("my-second-int-text")\n' +
14 'int_widget.add_class("my-second-int-text", selector="input")\n' +
15 15 'print(int_widget.model_id)\n');
16 16 this.execute_cell_then(int_text.index, function(index){
17 17 int_text.model_id = this.get_output_cell(index).text.trim();
18 18
19 19 this.test.assert(this.cell_element_exists(index,
20 20 '.widget-area .widget-subarea'),
21 21 'Widget subarea exists.');
22 22
23 23 this.test.assert(this.cell_element_exists(index, int_text.query),
24 24 'Widget int textbox exists.');
25 25
26 26 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
27 27 this.sendKeys(int_text.query, '1.05');
28 28 });
29 29
30 30 this.wait_for_widget(int_text);
31 31
32 32 index = this.append_cell('print(int_widget.value)\n');
33 33 this.execute_cell_then(index, function(index){
34 34 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
35 35 'Int textbox value set.');
36 36 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
37 37 this.sendKeys(int_text.query, '123456789');
38 38 });
39 39
40 40 this.wait_for_widget(int_text);
41 41
42 42 index = this.append_cell('print(int_widget.value)\n');
43 43 this.execute_cell_then(index, function(index){
44 44 this.test.assertEquals(this.get_output_cell(index).text, '123456789\n',
45 45 'Long int textbox value set (probably triggers throttling).');
46 46 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
47 47 this.sendKeys(int_text.query, '12hello');
48 48 });
49 49
50 50 this.wait_for_widget(int_text);
51 51
52 52 index = this.append_cell('print(int_widget.value)\n');
53 53 this.execute_cell_then(index, function(index){
54 54 this.test.assertEquals(this.get_output_cell(index).text, '12\n',
55 55 'Invald int textbox value caught and filtered.');
56 56 });
57 57
58 58 index = this.append_cell(
59 59 'from IPython.html import widgets\n' +
60 60 'from IPython.display import display, clear_output\n' +
61 61 'print("Success")');
62 62 this.execute_cell_then(index);
63 63
64 64
65 65 var slider_query = '.widget-area .widget-subarea .widget-hbox-single .slider';
66 66 var int_text2 = {};
67 67 int_text2.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-num-test-text';
68 68 int_text2.index = this.append_cell(
69 69 'intrange = [widgets.BoundedIntTextWidget(),\n' +
70 70 ' widgets.IntSliderWidget()]\n' +
71 71 '[display(intrange[i]) for i in range(2)]\n' +
72 'intrange[0].add_class("my-second-num-test-text")\n' +
72 'intrange[0].add_class("my-second-num-test-text", selector="input")\n' +
73 73 'print(intrange[0].model_id)\n');
74 74 this.execute_cell_then(int_text2.index, function(index){
75 75 int_text2.model_id = this.get_output_cell(index).text.trim();
76 76
77 77 this.test.assert(this.cell_element_exists(index,
78 78 '.widget-area .widget-subarea'),
79 79 'Widget subarea exists.');
80 80
81 81 this.test.assert(this.cell_element_exists(index, slider_query),
82 82 'Widget slider exists.');
83 83
84 84 this.test.assert(this.cell_element_exists(index, int_text2.query),
85 85 'Widget int textbox exists.');
86 86 });
87 87
88 88 index = this.append_cell(
89 89 'for widget in intrange:\n' +
90 90 ' widget.max = 50\n' +
91 91 ' widget.min = -50\n' +
92 92 ' widget.value = 25\n' +
93 93 'print("Success")\n');
94 94 this.execute_cell_then(index, function(index){
95 95
96 96 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
97 97 'Int range properties cell executed with correct output.');
98 98
99 99 this.test.assert(this.cell_element_exists(int_text2.index, slider_query),
100 100 'Widget slider exists.');
101 101
102 102 this.test.assert(this.cell_element_function(int_text2.index, slider_query,
103 103 'slider', ['value']) == 25,
104 104 'Slider set to Python value.');
105 105
106 106 this.test.assert(this.cell_element_function(int_text2.index, int_text2.query,
107 107 'val') == 25, 'Int textbox set to Python value.');
108 108
109 109 // Clear the int textbox value and then set it to 1 by emulating
110 110 // keyboard presses.
111 111 this.evaluate(function(q){
112 112 var textbox = IPython.notebook.element.find(q);
113 113 textbox.val('1');
114 114 textbox.trigger('keyup');
115 115 }, {q: int_text2.query});
116 116 });
117 117
118 118 this.wait_for_widget(int_text2);
119 119
120 120 index = this.append_cell('print(intrange[0].value)\n');
121 121 this.execute_cell_then(index, function(index){
122 122 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
123 123 'Int textbox set int range value');
124 124
125 125 // Clear the int textbox value and then set it to 120 by emulating
126 126 // keyboard presses.
127 127 this.evaluate(function(q){
128 128 var textbox = IPython.notebook.element.find(q);
129 129 textbox.val('120');
130 130 textbox.trigger('keyup');
131 131 }, {q: int_text2.query});
132 132 });
133 133
134 134 this.wait_for_widget(int_text2);
135 135
136 136 index = this.append_cell('print(intrange[0].value)\n');
137 137 this.execute_cell_then(index, function(index){
138 138 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
139 139 'Int textbox value bound');
140 140
141 141 // Clear the int textbox value and then set it to 'hello world' by
142 142 // emulating keyboard presses. 'hello world' should get filtered...
143 143 this.evaluate(function(q){
144 144 var textbox = IPython.notebook.element.find(q);
145 145 textbox.val('hello world');
146 146 textbox.trigger('keyup');
147 147 }, {q: int_text2.query});
148 148 });
149 149
150 150 this.wait_for_widget(int_text2);
151 151
152 152 index = this.append_cell('print(intrange[0].value)\n');
153 153 this.execute_cell_then(index, function(index){
154 154 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
155 155 'Invalid int textbox characters ignored');
156 156 });
157 157 }); No newline at end of file
@@ -1,449 +1,446 b''
1 1 """Base Widget class. Allows user to create widgets in the back-end that render
2 2 in the IPython notebook front-end.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15 from contextlib import contextmanager
16 16
17 17 from IPython.core.getipython import get_ipython
18 18 from IPython.kernel.comm import Comm
19 19 from IPython.config import LoggingConfigurable
20 20 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int
21 21 from IPython.utils.py3compat import string_types
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Classes
25 25 #-----------------------------------------------------------------------------
26 26 class CallbackDispatcher(LoggingConfigurable):
27 27 """A structure for registering and running callbacks"""
28 28 callbacks = List()
29 29
30 30 def __call__(self, *args, **kwargs):
31 31 """Call all of the registered callbacks."""
32 32 value = None
33 33 for callback in self.callbacks:
34 34 try:
35 35 local_value = callback(*args, **kwargs)
36 36 except Exception as e:
37 37 ip = get_ipython()
38 38 if ip is None:
39 39 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
40 40 else:
41 41 ip.showtraceback()
42 42 else:
43 43 value = local_value if local_value is not None else value
44 44 return value
45 45
46 46 def register_callback(self, callback, remove=False):
47 47 """(Un)Register a callback
48 48
49 49 Parameters
50 50 ----------
51 51 callback: method handle
52 52 Method to be registered or unregistered.
53 53 remove=False: bool
54 54 Whether to unregister the callback."""
55 55
56 56 # (Un)Register the callback.
57 57 if remove and callback in self.callbacks:
58 58 self.callbacks.remove(callback)
59 59 elif not remove and callback not in self.callbacks:
60 60 self.callbacks.append(callback)
61 61
62 62 def _show_traceback(method):
63 63 """decorator for showing tracebacks in IPython"""
64 64 def m(self, *args, **kwargs):
65 65 try:
66 66 return(method(self, *args, **kwargs))
67 67 except Exception as e:
68 68 ip = get_ipython()
69 69 if ip is None:
70 70 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
71 71 else:
72 72 ip.showtraceback()
73 73 return m
74 74
75 75 class Widget(LoggingConfigurable):
76 76 #-------------------------------------------------------------------------
77 77 # Class attributes
78 78 #-------------------------------------------------------------------------
79 79 _widget_construction_callback = None
80 80 widgets = {}
81 81
82 82 @staticmethod
83 83 def on_widget_constructed(callback):
84 84 """Registers a callback to be called when a widget is constructed.
85 85
86 86 The callback must have the following signature:
87 87 callback(widget)"""
88 88 Widget._widget_construction_callback = callback
89 89
90 90 @staticmethod
91 91 def _call_widget_constructed(widget):
92 92 """Static method, called when a widget is constructed."""
93 93 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
94 94 Widget._widget_construction_callback(widget)
95 95
96 96 #-------------------------------------------------------------------------
97 97 # Traits
98 98 #-------------------------------------------------------------------------
99 99 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
100 100 registered in the front-end to create and sync this widget with.""")
101 101 _view_name = Unicode(help="""Default view registered in the front-end
102 102 to use to represent the widget.""", sync=True)
103 103 _comm = Instance('IPython.kernel.comm.Comm')
104 104
105 105 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
106 106 front-end can send before receiving an idle msg from the back-end.""")
107 107
108 108 keys = List()
109 109 def _keys_default(self):
110 110 return [name for name in self.traits(sync=True)]
111 111
112 112 _property_lock = Tuple((None, None))
113 113
114 114 _display_callbacks = Instance(CallbackDispatcher, ())
115 115 _msg_callbacks = Instance(CallbackDispatcher, ())
116 116
117 117 #-------------------------------------------------------------------------
118 118 # (Con/de)structor
119 119 #-------------------------------------------------------------------------
120 120 def __init__(self, **kwargs):
121 121 """Public constructor"""
122 122 super(Widget, self).__init__(**kwargs)
123 123
124 124 self.on_trait_change(self._handle_property_changed, self.keys)
125 125 Widget._call_widget_constructed(self)
126 126
127 127 def __del__(self):
128 128 """Object disposal"""
129 129 self.close()
130 130
131 131 #-------------------------------------------------------------------------
132 132 # Properties
133 133 #-------------------------------------------------------------------------
134 134
135 135 @property
136 136 def comm(self):
137 137 """Gets the Comm associated with this widget.
138 138
139 139 If a Comm doesn't exist yet, a Comm will be created automagically."""
140 140 if self._comm is None:
141 141 # Create a comm.
142 142 self._comm = Comm(target_name=self._model_name)
143 143 self._comm.on_msg(self._handle_msg)
144 144 Widget.widgets[self.model_id] = self
145 145
146 146 # first update
147 147 self.send_state()
148 148 return self._comm
149 149
150 150 @property
151 151 def model_id(self):
152 152 """Gets the model id of this widget.
153 153
154 154 If a Comm doesn't exist yet, a Comm will be created automagically."""
155 155 return self.comm.comm_id
156 156
157 157 #-------------------------------------------------------------------------
158 158 # Methods
159 159 #-------------------------------------------------------------------------
160 160
161 161 def close(self):
162 162 """Close method.
163 163
164 164 Closes the underlying comm.
165 165 When the comm is closed, all of the widget views are automatically
166 166 removed from the front-end."""
167 167 if self._comm is not None:
168 168 Widget.widgets.pop(self.model_id, None)
169 169 self._comm.close()
170 170 self._comm = None
171 171
172 172 def send_state(self, key=None):
173 173 """Sends the widget state, or a piece of it, to the front-end.
174 174
175 175 Parameters
176 176 ----------
177 177 key : unicode (optional)
178 178 A single property's name to sync with the front-end.
179 179 """
180 180 self._send({
181 181 "method" : "update",
182 182 "state" : self.get_state()
183 183 })
184 184
185 185 def get_state(self, key=None):
186 186 """Gets the widget state, or a piece of it.
187 187
188 188 Parameters
189 189 ----------
190 190 key : unicode (optional)
191 191 A single property's name to get.
192 192 """
193 193 keys = self.keys if key is None else [key]
194 194 state = {}
195 195 for k in keys:
196 196 f = self.trait_metadata(k, 'to_json')
197 197 if f is None:
198 198 f = self._trait_to_json
199 199 value = getattr(self, k)
200 200 state[k] = f(value)
201 201 return state
202 202
203 203 def send(self, content):
204 204 """Sends a custom msg to the widget model in the front-end.
205 205
206 206 Parameters
207 207 ----------
208 208 content : dict
209 209 Content of the message to send.
210 210 """
211 211 self._send({"method": "custom", "content": content})
212 212
213 213 def on_msg(self, callback, remove=False):
214 214 """(Un)Register a custom msg receive callback.
215 215
216 216 Parameters
217 217 ----------
218 218 callback: callable
219 219 callback will be passed two arguments when a message arrives::
220 220
221 221 callback(widget, content)
222 222
223 223 remove: bool
224 224 True if the callback should be unregistered."""
225 225 self._msg_callbacks.register_callback(callback, remove=remove)
226 226
227 227 def on_displayed(self, callback, remove=False):
228 228 """(Un)Register a widget displayed callback.
229 229
230 230 Parameters
231 231 ----------
232 232 callback: method handler
233 233 Must have a signature of::
234 234
235 235 callback(widget, **kwargs)
236 236
237 237 kwargs from display are passed through without modification.
238 238 remove: bool
239 239 True if the callback should be unregistered."""
240 240 self._display_callbacks.register_callback(callback, remove=remove)
241 241
242 242 #-------------------------------------------------------------------------
243 243 # Support methods
244 244 #-------------------------------------------------------------------------
245 245 @contextmanager
246 246 def _lock_property(self, key, value):
247 247 """Lock a property-value pair.
248 248
249 249 NOTE: This, in addition to the single lock for all state changes, is
250 250 flawed. In the future we may want to look into buffering state changes
251 251 back to the front-end."""
252 252 self._property_lock = (key, value)
253 253 try:
254 254 yield
255 255 finally:
256 256 self._property_lock = (None, None)
257 257
258 258 def _should_send_property(self, key, value):
259 259 """Check the property lock (property_lock)"""
260 260 return key != self._property_lock[0] or \
261 261 value != self._property_lock[1]
262 262
263 263 # Event handlers
264 264 @_show_traceback
265 265 def _handle_msg(self, msg):
266 266 """Called when a msg is received from the front-end"""
267 267 data = msg['content']['data']
268 268 method = data['method']
269 269 if not method in ['backbone', 'custom']:
270 270 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
271 271
272 272 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
273 273 if method == 'backbone' and 'sync_data' in data:
274 274 sync_data = data['sync_data']
275 275 self._handle_receive_state(sync_data) # handles all methods
276 276
277 277 # Handle a custom msg from the front-end
278 278 elif method == 'custom':
279 279 if 'content' in data:
280 280 self._handle_custom_msg(data['content'])
281 281
282 282 def _handle_receive_state(self, sync_data):
283 283 """Called when a state is received from the front-end."""
284 284 for name in self.keys:
285 285 if name in sync_data:
286 286 f = self.trait_metadata(name, 'from_json')
287 287 if f is None:
288 288 f = self._trait_from_json
289 289 value = f(sync_data[name])
290 290 with self._lock_property(name, value):
291 291 setattr(self, name, value)
292 292
293 293 def _handle_custom_msg(self, content):
294 294 """Called when a custom msg is received."""
295 295 self._msg_callbacks(self, content)
296 296
297 297 def _handle_property_changed(self, name, old, new):
298 298 """Called when a property has been changed."""
299 299 # Make sure this isn't information that the front-end just sent us.
300 300 if self._should_send_property(name, new):
301 301 # Send new state to front-end
302 302 self.send_state(key=name)
303 303
304 304 def _handle_displayed(self, **kwargs):
305 305 """Called when a view has been displayed for this widget instance"""
306 306 self._display_callbacks(self, **kwargs)
307 307
308 308 def _trait_to_json(self, x):
309 309 """Convert a trait value to json
310 310
311 311 Traverse lists/tuples and dicts and serialize their values as well.
312 312 Replace any widgets with their model_id
313 313 """
314 314 if isinstance(x, dict):
315 315 return {k: self._trait_to_json(v) for k, v in x.items()}
316 316 elif isinstance(x, (list, tuple)):
317 317 return [self._trait_to_json(v) for v in x]
318 318 elif isinstance(x, Widget):
319 319 return "IPY_MODEL_" + x.model_id
320 320 else:
321 321 return x # Value must be JSON-able
322 322
323 323 def _trait_from_json(self, x):
324 324 """Convert json values to objects
325 325
326 326 Replace any strings representing valid model id values to Widget references.
327 327 """
328 328 if isinstance(x, dict):
329 329 return {k: self._trait_from_json(v) for k, v in x.items()}
330 330 elif isinstance(x, (list, tuple)):
331 331 return [self._trait_from_json(v) for v in x]
332 332 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
333 333 # we want to support having child widgets at any level in a hierarchy
334 334 # trusting that a widget UUID will not appear out in the wild
335 335 return Widget.widgets[x]
336 336 else:
337 337 return x
338 338
339 339 def _ipython_display_(self, **kwargs):
340 340 """Called when `IPython.display.display` is called on the widget."""
341 341 # Show view. By sending a display message, the comm is opened and the
342 342 # initial state is sent.
343 343 self._send({"method": "display"})
344 344 self._handle_displayed(**kwargs)
345 345
346 346 def _send(self, msg):
347 347 """Sends a message to the model in the front-end."""
348 348 self.comm.send(msg)
349 349
350 350
351 351 class DOMWidget(Widget):
352 352 visible = Bool(True, help="Whether the widget is visible.", sync=True)
353 353 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
354 354
355 355 def get_css(self, key, selector=""):
356 356 """Get a CSS property of the widget.
357 357
358 358 Note: This function does not actually request the CSS from the
359 359 front-end; Only properties that have been set with set_css can be read.
360 360
361 361 Parameters
362 362 ----------
363 363 key: unicode
364 364 CSS key
365 365 selector: unicode (optional)
366 366 JQuery selector used when the CSS key/value was set.
367 367 """
368 368 if selector in self._css and key in self._css[selector]:
369 369 return self._css[selector][key]
370 370 else:
371 371 return None
372 372
373 373 def set_css(self, dict_or_key, value=None, selector=''):
374 374 """Set one or more CSS properties of the widget.
375 375
376 376 This function has two signatures:
377 377 - set_css(css_dict, selector='')
378 378 - set_css(key, value, selector='')
379 379
380 380 Parameters
381 381 ----------
382 382 css_dict : dict
383 383 CSS key/value pairs to apply
384 384 key: unicode
385 385 CSS key
386 386 value:
387 387 CSS value
388 388 selector: unicode (optional, kwarg only)
389 389 JQuery selector to use to apply the CSS key/value. If no selector
390 390 is provided, an empty selector is used. An empty selector makes the
391 front-end try to apply the css to a default element. The default
392 element is an attribute unique to each view, which is a DOM element
393 of the view that should be styled with common CSS (see
394 `$el_to_style` in the Javascript code).
391 front-end try to apply the css to the top-level element.
395 392 """
396 393 if value is None:
397 394 css_dict = dict_or_key
398 395 else:
399 396 css_dict = {dict_or_key: value}
400 397
401 398 for (key, value) in css_dict.items():
402 399 # First remove the selector/key pair from the css list if it exists.
403 400 # Then add the selector/key pair and new value to the bottom of the
404 401 # list.
405 402 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
406 403 self._css += [(selector, key, value)]
407 404 self.send_state('_css')
408 405
409 406 def add_class(self, class_names, selector=""):
410 407 """Add class[es] to a DOM element.
411 408
412 409 Parameters
413 410 ----------
414 411 class_names: unicode or list
415 412 Class name(s) to add to the DOM element(s).
416 413 selector: unicode (optional)
417 414 JQuery selector to select the DOM element(s) that the class(es) will
418 415 be added to.
419 416 """
420 417 class_list = class_names
421 418 if isinstance(class_list, (list, tuple)):
422 419 class_list = ' '.join(class_list)
423 420
424 421 self.send({
425 422 "msg_type" : "add_class",
426 423 "class_list" : class_list,
427 424 "selector" : selector
428 425 })
429 426
430 427 def remove_class(self, class_names, selector=""):
431 428 """Remove class[es] from a DOM element.
432 429
433 430 Parameters
434 431 ----------
435 432 class_names: unicode or list
436 433 Class name(s) to remove from the DOM element(s).
437 434 selector: unicode (optional)
438 435 JQuery selector to select the DOM element(s) that the class(es) will
439 436 be removed from.
440 437 """
441 438 class_list = class_names
442 439 if isinstance(class_list, (list, tuple)):
443 440 class_list = ' '.join(class_list)
444 441
445 442 self.send({
446 443 "msg_type" : "remove_class",
447 444 "class_list" : class_list,
448 445 "selector" : selector,
449 446 })
General Comments 0
You need to be logged in to leave comments. Login now