##// END OF EJS Templates
Fixed context errors and a couple of typos to get the tests working again
Jonathan Frederic -
Show More
@@ -1,189 +1,189 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // WidgetModel, WidgetView, and WidgetManager
10 10 //============================================================================
11 11 /**
12 12 * Base Widget classes
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule widget
16 16 */
17 17
18 18 (function () {
19 19 "use strict";
20 20
21 21 // Use require.js 'define' method so that require.js is intelligent enough to
22 22 // syncronously load everything within this file when it is being 'required'
23 23 // elsewhere.
24 24 define(["underscore",
25 25 "backbone",
26 26 ], function (Underscore, Backbone) {
27 27
28 28 //--------------------------------------------------------------------
29 29 // WidgetManager class
30 30 //--------------------------------------------------------------------
31 31 var WidgetManager = function (comm_manager) {
32 32 // Public constructor
33 33 WidgetManager._managers.push(this);
34 34
35 35 // Attach a comm manager to the
36 36 this.comm_manager = comm_manager;
37 37 this._models = {}; /* Dictionary of model ids and model instances */
38 38
39 39 // Register already-registered widget model types with the comm manager.
40 40 var that = this;
41 _.each(WidgetManager._model_types, function(value, key) {
42 that.comm_manager.register_target(value, $.proxy(that._handle_comm_open, that));
41 _.each(WidgetManager._model_types, function(model_type, model_name) {
42 that.comm_manager.register_target(model_name, $.proxy(that._handle_comm_open, that));
43 43 });
44 44 };
45 45
46 46 //--------------------------------------------------------------------
47 47 // Class level
48 48 //--------------------------------------------------------------------
49 49 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
50 50 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
51 51 WidgetManager._managers = []; /* List of widget managers */
52 52
53 53 WidgetManager.register_widget_model = function (model_name, model_type) {
54 54 // Registers a widget model by name.
55 55 WidgetManager._model_types[model_name] = model_type;
56 56
57 57 // Register the widget with the comm manager. Make sure to pass this object's context
58 58 // in so `this` works in the call back.
59 59 _.each(WidgetManager._managers, function(instance, i) {
60 60 if (instance.comm_manager !== null) {
61 61 instance.comm_manager.register_target(model_name, $.proxy(instance._handle_comm_open, instance));
62 62 }
63 63 });
64 64 };
65 65
66 66 WidgetManager.register_widget_view = function (view_name, view_type) {
67 67 // Registers a widget view by name.
68 68 WidgetManager._view_types[view_name] = view_type;
69 69 };
70 70
71 71 //--------------------------------------------------------------------
72 72 // Instance level
73 73 //--------------------------------------------------------------------
74 74 WidgetManager.prototype.display_view = function(msg, model) {
75 75 var cell = this.get_msg_cell(msg.parent_header.msg_id);
76 76 if (cell === null) {
77 77 console.log("Could not determine where the display" +
78 78 " message was from. Widget will not be displayed");
79 79 } else {
80 80 var view = this.create_view(model, {cell: cell});
81 81 if (view === null) {
82 82 console.error("View creation failed", model);
83 83 }
84 84 if (cell.widget_subarea !== undefined
85 85 && cell.widget_subarea !== null) {
86 86
87 87 cell.widget_area.show();
88 88 cell.widget_subarea.append(view.$el);
89 89 }
90 90 }
91 91 },
92 92
93 93 WidgetManager.prototype.create_view = function(model, options) {
94 94 var view_name = model.get('view_name');
95 95 var ViewType = WidgetManager._view_types[view_name];
96 96 if (ViewType !== undefined && ViewType !== null) {
97 97 var parameters = {model: model, options: options};
98 98 var view = new ViewType(parameters);
99 99 view.render();
100 100 IPython.keyboard_manager.register_events(view.$el);
101 101 model.views.push(view);
102 102 model.on('destroy', view.remove, view);
103 103 return view;
104 104 }
105 105 return null;
106 106 },
107 107
108 108 WidgetManager.prototype.get_msg_cell = function (msg_id) {
109 109 var cell = null;
110 110 // First, check to see if the msg was triggered by cell execution.
111 111 if (IPython.notebook !== undefined && IPython.notebook !== null) {
112 112 cell = IPython.notebook.get_msg_cell(msg_id);
113 113 }
114 114 if (cell !== null) {
115 115 return cell
116 116 }
117 117 // Second, check to see if a get_cell callback was defined
118 118 // for the message. get_cell callbacks are registered for
119 119 // widget messages, so this block is actually checking to see if the
120 120 // message was triggered by a widget.
121 121 var kernel = this.comm_manager.kernel;
122 122 if (kernel !== undefined && kernel !== null) {
123 123 var callbacks = kernel.get_callbacks_for_msg(msg_id);
124 124 if (callbacks !== undefined &&
125 125 callbacks.iopub !== undefined &&
126 126 callbacks.iopub.get_cell !== undefined) {
127 127
128 128 return callbacks.iopub.get_cell();
129 129 }
130 130 }
131 131
132 132 // Not triggered by a cell or widget (no get_cell callback
133 133 // exists).
134 134 return null;
135 135 };
136 136
137 137 WidgetManager.prototype.callbacks = function (view) {
138 138 // callback handlers specific a view
139 139 var callbacks = {};
140 140 var cell = view.options.cell;
141 141 if (cell !== null) {
142 142 // Try to get output handlers
143 143 var handle_output = null;
144 144 var handle_clear_output = null;
145 145 if (cell.output_area !== undefined && cell.output_area !== null) {
146 146 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
147 147 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
148 148 }
149 149
150 150 // Create callback dict using what is known
151 151 var that = this;
152 152 callbacks = {
153 153 iopub : {
154 154 output : handle_output,
155 155 clear_output : handle_clear_output,
156 156
157 157 // Special function only registered by widget messages.
158 158 // Allows us to get the cell for a message so we know
159 159 // where to add widgets if the code requires it.
160 160 get_cell : function () {
161 161 return cell;
162 162 },
163 163 },
164 164 };
165 165 }
166 166 return callbacks;
167 167 };
168 168
169 169 WidgetManager.prototype.get_model = function (model_id) {
170 170 // Look-up a model instance by its id.
171 171 var model = this._models[model_id];
172 172 if (model !== undefined && model.id == model_id) {
173 173 return model;
174 174 }
175 175 return null;
176 176 };
177 177
178 178 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
179 179 // Handle when a comm is opened.
180 180 var model_id = comm.comm_id;
181 181 var widget_type_name = msg.content.target_name;
182 182 var widget_model = new WidgetManager._model_types[widget_type_name](this, model_id, comm);
183 183 this._models[model_id] = widget_model;
184 184 };
185 185
186 186 IPython.WidgetManager = WidgetManager;
187 187 return IPython.WidgetManager;
188 188 });
189 189 }());
@@ -1,407 +1,411 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Base Widget Model and View classes
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["notebook/js/widgetmanager",
18 18 "underscore",
19 19 "backbone"],
20 20 function(WidgetManager, Underscore, Backbone){
21 21
22 22 var WidgetModel = Backbone.Model.extend({
23 23 constructor: function (widget_manager, model_id, comm) {
24 24 // Construcctor
25 25 //
26 26 // Creates a WidgetModel instance.
27 27 //
28 28 // Parameters
29 29 // ----------
30 30 // widget_manager : WidgetManager instance
31 31 // model_id : string
32 32 // An ID unique to this model.
33 33 // comm : Comm instance (optional)
34 34 this.widget_manager = widget_manager;
35 35 this.pending_msgs = 0;
36 36 this.msg_throttle = 2;
37 37 this.msg_buffer = null;
38 38 this.key_value_lock = null;
39 39 this.id = model_id;
40 40 this.views = [];
41 41
42 42 if (comm !== undefined) {
43 43 // Remember comm associated with the model.
44 44 this.comm = comm;
45 45 comm.model = this;
46 46
47 47 // Hook comm messages up to model.
48 48 comm.on_close($.proxy(this._handle_comm_closed, this));
49 49 comm.on_msg($.proxy(this._handle_comm_msg, this));
50 50 }
51 51 return Backbone.Model.apply(this);
52 52 },
53 53
54 54 send: function (content, callbacks) {
55 55 // Send a custom msg over the comm.
56 56 if (this.comm !== undefined) {
57 57 var data = {method: 'custom', content: content};
58 58 this.comm.send(data, callbacks);
59 59 }
60 60 },
61 61
62 62 _handle_comm_closed: function (msg) {
63 63 // Handle when a widget is closed.
64 64 this.trigger('comm:close');
65 65 delete this.comm.model; // Delete ref so GC will collect widget model.
66 66 delete this.comm;
67 67 delete this.model_id; // Delete id from model so widget manager cleans up.
68 68 // TODO: Handle deletion, like this.destroy(), and delete views, etc.
69 69 },
70 70
71 71 _handle_comm_msg: function (msg) {
72 72 // Handle incoming comm msg.
73 73 var method = msg.content.data.method;
74 74 switch (method) {
75 75 case 'update':
76 76 this.apply_update(msg.content.data.state);
77 77 break;
78 78 case 'custom':
79 79 this.trigger('msg:custom', msg.content.data.content);
80 80 break;
81 81 case 'display':
82 82 this.widget_manager.display_view(msg, this);
83 83 break;
84 84 }
85 85 },
86 86
87 87 apply_update: function (state) {
88 88 // Handle when a widget is updated via the python side.
89 var that = this;
89 90 _.each(state, function(value, key) {
90 this.key_value_lock = [key, value];
91 that.key_value_lock = [key, value];
91 92 try {
92 this.set(key, this._unpack_models(value));
93 that.set(key, that._unpack_models(value));
93 94 } finally {
94 this.key_value_lock = null;
95 that.key_value_lock = null;
95 96 }
96 97 });
97 98 },
98 99
99 100 _handle_status: function (msg, callbacks) {
100 101 // Handle status msgs.
101 102
102 103 // execution_state : ('busy', 'idle', 'starting')
103 104 if (this.comm !== undefined) {
104 105 if (msg.content.execution_state ==='idle') {
105 106 // Send buffer if this message caused another message to be
106 107 // throttled.
107 108 if (this.msg_buffer !== null &&
108 109 this.msg_throttle === this.pending_msgs) {
109 110 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
110 111 this.comm.send(data, callbacks);
111 112 this.msg_buffer = null;
112 113 } else {
113 114 --this.pending_msgs;
114 115 }
115 116 }
116 117 }
117 118 },
118 119
119 120 callbacks: function(view) {
120 121 // Create msg callbacks for a comm msg.
121 122 var callbacks = this.widget_manager.callbacks(view);
122 123
123 124 if (callbacks.iopub === undefined) {
124 125 callbacks.iopub = {};
125 126 }
126 127
127 128 var that = this;
128 129 callbacks.iopub.status = function (msg) {
129 130 that._handle_status(msg, callbacks);
130 131 }
131 132 return callbacks;
132 133 },
133 134
134 135 sync: function (method, model, options) {
135 136 // Handle sync to the back-end. Called when a model.save() is called.
136 137
137 138 // Make sure a comm exists.
138 139 var error = options.error || function() {
139 140 console.error('Backbone sync error:', arguments);
140 141 }
141 142 if (this.comm === undefined) {
142 143 error();
143 144 return false;
144 145 }
145 146
146 147 // Delete any key value pairs that the back-end already knows about.
147 148 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
148 149 if (this.key_value_lock !== null) {
149 150 var key = this.key_value_lock[0];
150 151 var value = this.key_value_lock[1];
151 152 if (attrs[key] === value) {
152 153 delete attrs[key];
153 154 }
154 155 }
155 156
156 157 // Only sync if there are attributes to send to the back-end.
157 if (attr.length > 0) {
158 if (_.size(attrs) > 0) {
158 159 var callbacks = options.callbacks || {};
159 160 if (this.pending_msgs >= this.msg_throttle) {
160 161 // The throttle has been exceeded, buffer the current msg so
161 162 // it can be sent once the kernel has finished processing
162 163 // some of the existing messages.
163 164
164 165 // Combine updates if it is a 'patch' sync, otherwise replace updates
165 166 switch (method) {
166 167 case 'patch':
167 168 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
168 169 break;
169 170 case 'update':
170 171 case 'create':
171 172 this.msg_buffer = attrs;
172 173 break;
173 174 default:
174 175 error();
175 176 return false;
176 177 }
177 178 this.msg_buffer_callbacks = callbacks;
178 179
179 180 } else {
180 181 // We haven't exceeded the throttle, send the message like
181 182 // normal. If this is a patch operation, just send the
182 183 // changes.
183 184 var data = {method: 'backbone', sync_data: attrs};
184 185 this.comm.send(data, callbacks);
185 186 this.pending_msgs++;
186 187 }
187 188 }
188 189 // Since the comm is a one-way communication, assume the message
189 190 // arrived. Don't call success since we don't have a model back from the server
190 191 // this means we miss out on the 'sync' event.
191 192 },
192 193
193 194 save_changes: function(callbacks) {
194 195 // Push this model's state to the back-end
195 196 //
196 197 // This invokes a Backbone.Sync.
197 198 this.save(this.changedAttributes(), {patch: true, callbacks: callbacks});
198 199 },
199 200
200 201 _pack_models: function(value) {
201 202 // Replace models with model ids recursively.
202 203 if (value instanceof Backbone.Model) {
203 204 return value.id;
204 205 } else if (value instanceof Object) {
205 206 var packed = {};
207 var that = this;
206 208 _.each(value, function(sub_value, key) {
207 packed[key] = this._pack_models(sub_value);
209 packed[key] = that._pack_models(sub_value);
208 210 });
209 211 return packed;
210 212 } else {
211 213 return value;
212 214 }
213 215 },
214 216
215 217 _unpack_models: function(value) {
216 218 // Replace model ids with models recursively.
217 219 if (value instanceof Object) {
218 220 var unpacked = {};
221 var that = this;
219 222 _.each(value, function(sub_value, key) {
220 unpacked[key] = this._unpack_models(sub_value);
223 unpacked[key] = that._unpack_models(sub_value);
221 224 });
222 225 return unpacked;
223 226 } else {
224 227 var model = this.widget_manager.get_model(value);
225 228 if (model !== null) {
226 229 return model;
227 230 } else {
228 231 return value;
229 232 }
230 233 }
231 234 },
232 235
233 236 });
234 237 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
235 238
236 239
237 240 var WidgetView = Backbone.View.extend({
238 241 initialize: function(parameters) {
239 242 // Public constructor.
240 243 this.model.on('change',this.update,this);
241 244 this.options = parameters.options;
242 245 this.child_views = [];
243 246 this.model.views.push(this);
244 247 },
245 248
246 249 update: function(){
247 250 // Triggered on model change.
248 251 //
249 252 // Update view to be consistent with this.model
250 253 },
251 254
252 255 create_child_view: function(child_model, options) {
253 256 // Create and return a child view.
254 257 //
255 258 // -given a model and (optionally) a view name if the view name is
256 259 // not given, it defaults to the model's default view attribute.
257 260
258 261 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
259 262 // it would be great to have the widget manager add the cell metadata
260 263 // to the subview without having to add it here.
261 264 var child_view = this.model.widget_manager.create_view(child_model, options || {});
262 265 this.child_views[child_model.id] = child_view;
263 266 return child_view;
264 267 },
265 268
266 269 delete_child_view: function(child_model, options) {
267 270 // Delete a child view that was previously created using create_child_view.
268 271 var view = this.child_views[child_model.id];
269 272 if (view !== undefined) {
270 273 delete this.child_views[child_model.id];
271 274 view.remove();
272 275 }
273 276 },
274 277
275 278 do_diff: function(old_list, new_list, removed_callback, added_callback) {
276 279 // Difference a changed list and call remove and add callbacks for
277 280 // each removed and added item in the new list.
278 281 //
279 282 // Parameters
280 283 // ----------
281 284 // old_list : array
282 285 // new_list : array
283 286 // removed_callback : Callback(item)
284 287 // Callback that is called for each item removed.
285 288 // added_callback : Callback(item)
286 289 // Callback that is called for each item added.
287 290
288 291
289 292 // removed items
290 293 _.each(_.difference(old_list, new_list), function(item, index, list) {
291 294 removed_callback(item);
292 295 }, this);
293 296
294 297 // added items
295 298 _.each(_.difference(new_list, old_list), function(item, index, list) {
296 299 added_callback(item);
297 300 }, this);
298 301 },
299 302
300 303 callbacks: function(){
301 304 // Create msg callbacks for a comm msg.
302 305 return this.model.callbacks(this);
303 306 },
304 307
305 308 render: function(){
306 309 // Render the view.
307 310 //
308 311 // By default, this is only called the first time the view is created
309 312 },
310 313
311 314 send: function (content) {
312 315 // Send a custom msg associated with this view.
313 316 this.model.send(content, this.callbacks());
314 317 },
315 318
316 319 touch: function () {
317 320 this.model.save_changes(this.callbacks());
318 321 },
319 322 });
320 323
321 324
322 325 var DOMWidgetView = WidgetView.extend({
323 326 initialize: function (options) {
324 327 // Public constructor
325 328
326 329 // In the future we may want to make changes more granular
327 330 // (e.g., trigger on visible:change).
328 331 this.model.on('change', this.update, this);
329 332 this.model.on('msg:custom', this.on_msg, this);
330 333 DOMWidgetView.__super__.initialize.apply(this, arguments);
331 334 },
332 335
333 336 on_msg: function(msg) {
334 337 // Handle DOM specific msgs.
335 338 switch(msg.msg_type) {
336 339 case 'add_class':
337 340 this.add_class(msg.selector, msg.class_list);
338 341 break;
339 342 case 'remove_class':
340 343 this.remove_class(msg.selector, msg.class_list);
341 344 break;
342 345 }
343 346 },
344 347
345 348 add_class: function (selector, class_list) {
346 349 // Add a DOM class to an element.
347 350 this._get_selector_element(selector).addClass(class_list);
348 351 },
349 352
350 353 remove_class: function (selector, class_list) {
351 354 // Remove a DOM class from an element.
352 355 this._get_selector_element(selector).removeClass(class_list);
353 356 },
354 357
355 358 update: function () {
356 359 // Update the contents of this view
357 360 //
358 361 // Called when the model is changed. The model may have been
359 362 // changed by another view or by a state update from the back-end.
360 363 // The very first update seems to happen before the element is
361 364 // finished rendering so we use setTimeout to give the element time
362 365 // to render
363 366 var e = this.$el;
364 367 var visible = this.model.get('visible');
365 368 setTimeout(function() {e.toggle(visible)},0);
366 369
367 370 var css = this.model.get('_css');
368 371 if (css === undefined) {return;}
372 var that = this;
369 373 _.each(css, function(css_traits, selector){
370 374 // Apply the css traits to all elements that match the selector.
371 var elements = this._get_selector_element(selector);
375 var elements = that._get_selector_element(selector);
372 376 if (elements.length > 0) {
373 377 _.each(css_traits, function(css_value, css_key){
374 378 elements.css(css_key, css_value);
375 379 });
376 380 }
377 381 });
378 382
379 383 },
380 384
381 385 _get_selector_element: function (selector) {
382 386 // Get the elements via the css selector.
383 387
384 388 // If the selector is blank, apply the style to the $el_to_style
385 389 // element. If the $el_to_style element is not defined, use apply
386 390 // the style to the view's element.
387 391 var elements;
388 392 if (selector === undefined || selector === null || selector === '') {
389 393 if (this.$el_to_style === undefined) {
390 394 elements = this.$el;
391 395 } else {
392 396 elements = this.$el_to_style;
393 397 }
394 398 } else {
395 399 elements = this.$el.find(selector);
396 400 }
397 401 return elements;
398 402 },
399 403 });
400 404
401 405 IPython.WidgetModel = WidgetModel;
402 406 IPython.WidgetView = WidgetView;
403 407 IPython.DOMWidgetView = DOMWidgetView;
404 408
405 409 // Pass through WidgetManager namespace.
406 410 return WidgetManager;
407 411 });
@@ -1,267 +1,268 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // FloatWidget
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["notebook/js/widgets/widget"], function(WidgetManager){
18 18
19 19 var FloatSliderView = IPython.DOMWidgetView.extend({
20 20 render : function(){
21 21 // Called when view is rendered.
22 22 this.$el
23 23 .addClass('widget-hbox-single');
24 24 this.$label = $('<div />')
25 25 .appendTo(this.$el)
26 26 .addClass('widget-hlabel')
27 27 .hide();
28 28 this.$slider = $('<div />')
29 29 .slider({})
30 30 .addClass('slider');
31 31
32 32 // Put the slider in a container
33 33 this.$slider_container = $('<div />')
34 34 .addClass('widget-hslider')
35 35 .append(this.$slider);
36 36 this.$el_to_style = this.$slider_container; // Set default element to style
37 37 this.$el.append(this.$slider_container);
38 38
39 39 // Set defaults.
40 40 this.update();
41 41 },
42 42
43 43 update : function(options){
44 44 // Update the contents of this view
45 45 //
46 46 // Called when the model is changed. The model may have been
47 47 // changed by another view or by a state update from the back-end.
48 48
49 49 if (options === undefined || options.updated_view != this) {
50 50 // JQuery slider option keys. These keys happen to have a
51 51 // one-to-one mapping with the corrosponding keys of the model.
52 52 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
53 var that = this;
53 54 _.each(jquery_slider_keys, function(key, i) {
54 var model_value = this.model.get(key);
55 var model_value = that.model.get(key);
55 56 if (model_value !== undefined) {
56 this.$slider.slider("option", key, model_value);
57 that.$slider.slider("option", key, model_value);
57 58 }
58 59 });
59 60
60 61 // WORKAROUND FOR JQUERY SLIDER BUG.
61 62 // The horizontal position of the slider handle
62 63 // depends on the value of the slider at the time
63 64 // of orientation change. Before applying the new
64 65 // workaround, we set the value to the minimum to
65 66 // make sure that the horizontal placement of the
66 67 // handle in the vertical slider is always
67 68 // consistent.
68 69 var orientation = this.model.get('orientation');
69 70 var value = this.model.get('min');
70 71 this.$slider.slider('option', 'value', value);
71 72 this.$slider.slider('option', 'orientation', orientation);
72 73 value = this.model.get('value');
73 74 this.$slider.slider('option', 'value', value);
74 75
75 76 // Use the right CSS classes for vertical & horizontal sliders
76 77 if (orientation=='vertical') {
77 78 this.$slider_container
78 79 .removeClass('widget-hslider')
79 80 .addClass('widget-vslider');
80 81 this.$el
81 82 .removeClass('widget-hbox-single')
82 83 .addClass('widget-vbox-single');
83 84 this.$label
84 85 .removeClass('widget-hlabel')
85 86 .addClass('widget-vlabel');
86 87
87 88 } else {
88 89 this.$slider_container
89 90 .removeClass('widget-vslider')
90 91 .addClass('widget-hslider');
91 92 this.$el
92 93 .removeClass('widget-vbox-single')
93 94 .addClass('widget-hbox-single');
94 95 this.$label
95 96 .removeClass('widget-vlabel')
96 97 .addClass('widget-hlabel');
97 98 }
98 99
99 100 var description = this.model.get('description');
100 101 if (description.length === 0) {
101 102 this.$label.hide();
102 103 } else {
103 104 this.$label.text(description);
104 105 this.$label.show();
105 106 }
106 107 }
107 108 return FloatSliderView.__super__.update.apply(this);
108 109 },
109 110
110 111 events: {
111 112 // Dictionary of events and their handlers.
112 113 "slide" : "handleSliderChange"
113 114 },
114 115
115 116 handleSliderChange: function(e, ui) {
116 117 // Handle when the slider value is changed.
117 118
118 119 // Calling model.set will trigger all of the other views of the
119 120 // model to update.
120 121 this.model.set('value', ui.value, {updated_view: this});
121 122 this.touch();
122 123 },
123 124 });
124 125 WidgetManager.register_widget_view('FloatSliderView', FloatSliderView);
125 126
126 127
127 128 var FloatTextView = IPython.DOMWidgetView.extend({
128 129 render : function(){
129 130 // Called when view is rendered.
130 131 this.$el
131 132 .addClass('widget-hbox-single');
132 133 this.$label = $('<div />')
133 134 .appendTo(this.$el)
134 135 .addClass('widget-hlabel')
135 136 .hide();
136 137 this.$textbox = $('<input type="text" />')
137 138 .addClass('input')
138 139 .addClass('widget-numeric-text')
139 140 .appendTo(this.$el);
140 141 this.$el_to_style = this.$textbox; // Set default element to style
141 142 this.update(); // Set defaults.
142 143 },
143 144
144 145 update : function(options){
145 146 // Update the contents of this view
146 147 //
147 148 // Called when the model is changed. The model may have been
148 149 // changed by another view or by a state update from the back-end.
149 150 if (options === undefined || options.updated_view != this) {
150 151 var value = this.model.get('value');
151 152 if (parseFloat(this.$textbox.val()) != value) {
152 153 this.$textbox.val(value);
153 154 }
154 155
155 156 if (this.model.get('disabled')) {
156 157 this.$textbox.attr('disabled','disabled');
157 158 } else {
158 159 this.$textbox.removeAttr('disabled');
159 160 }
160 161
161 162 var description = this.model.get('description');
162 163 if (description.length === 0) {
163 164 this.$label.hide();
164 165 } else {
165 166 this.$label.text(description);
166 167 this.$label.show();
167 168 }
168 169 }
169 170 return FloatTextView.__super__.update.apply(this);
170 171 },
171 172
172 173 events: {
173 174 // Dictionary of events and their handlers.
174 175
175 176 "keyup input" : "handleChanging",
176 177 "paste input" : "handleChanging",
177 178 "cut input" : "handleChanging",
178 179
179 180 // Fires only when control is validated or looses focus.
180 181 "change input" : "handleChanged"
181 182 },
182 183
183 184 handleChanging: function(e) {
184 185 // Handles and validates user input.
185 186
186 187 // Try to parse value as a float.
187 188 var numericalValue = 0.0;
188 189 if (e.target.value !== '') {
189 190 numericalValue = parseFloat(e.target.value);
190 191 }
191 192
192 193 // If parse failed, reset value to value stored in model.
193 194 if (isNaN(numericalValue)) {
194 195 e.target.value = this.model.get('value');
195 196 } else if (!isNaN(numericalValue)) {
196 197 if (this.model.get('max') !== undefined) {
197 198 numericalValue = Math.min(this.model.get('max'), numericalValue);
198 199 }
199 200 if (this.model.get('min') !== undefined) {
200 201 numericalValue = Math.max(this.model.get('min'), numericalValue);
201 202 }
202 203
203 204 // Apply the value if it has changed.
204 205 if (numericalValue != this.model.get('value')) {
205 206
206 207 // Calling model.set will trigger all of the other views of the
207 208 // model to update.
208 209 this.model.set('value', numericalValue, {updated_view: this});
209 210 this.touch();
210 211 }
211 212 }
212 213 },
213 214
214 215 handleChanged: function(e) {
215 216 // Applies validated input.
216 217 if (this.model.get('value') != e.target.value) {
217 218 e.target.value = this.model.get('value');
218 219 }
219 220 }
220 221 });
221 222 WidgetManager.register_widget_view('FloatTextView', FloatTextView);
222 223
223 224
224 225 var ProgressView = IPython.DOMWidgetView.extend({
225 226 render : function(){
226 227 // Called when view is rendered.
227 228 this.$el
228 229 .addClass('widget-hbox-single');
229 230 this.$label = $('<div />')
230 231 .appendTo(this.$el)
231 232 .addClass('widget-hlabel')
232 233 .hide();
233 234 this.$progress = $('<div />')
234 235 .addClass('progress')
235 236 .addClass('widget-progress')
236 237 .appendTo(this.$el);
237 238 this.$el_to_style = this.$progress; // Set default element to style
238 239 this.$bar = $('<div />')
239 240 .addClass('bar')
240 241 .css('width', '50%')
241 242 .appendTo(this.$progress);
242 243 this.update(); // Set defaults.
243 244 },
244 245
245 246 update : function(){
246 247 // Update the contents of this view
247 248 //
248 249 // Called when the model is changed. The model may have been
249 250 // changed by another view or by a state update from the back-end.
250 251 var value = this.model.get('value');
251 252 var max = this.model.get('max');
252 253 var min = this.model.get('min');
253 254 var percent = 100.0 * (value - min) / (max - min);
254 255 this.$bar.css('width', percent + '%');
255 256
256 257 var description = this.model.get('description');
257 258 if (description.length === 0) {
258 259 this.$label.hide();
259 260 } else {
260 261 this.$label.text(description);
261 262 this.$label.show();
262 263 }
263 264 return ProgressView.__super__.update.apply(this);
264 265 },
265 266 });
266 267 WidgetManager.register_widget_view('ProgressView', ProgressView);
267 268 });
@@ -1,220 +1,221 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // IntWidget
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["notebook/js/widgets/widget"], function(WidgetManager){
18 18
19 19 var IntSliderView = IPython.DOMWidgetView.extend({
20 20 render : function(){
21 21 // Called when view is rendered.
22 22 this.$el
23 23 .addClass('widget-hbox-single');
24 24 this.$label = $('<div />')
25 25 .appendTo(this.$el)
26 26 .addClass('widget-hlabel')
27 27 .hide();
28 28 this.$slider = $('<div />')
29 29 .slider({})
30 30 .addClass('slider');
31 31
32 32 // Put the slider in a container
33 33 this.$slider_container = $('<div />')
34 34 .addClass('widget-hslider')
35 35 .append(this.$slider);
36 36 this.$el_to_style = this.$slider_container; // Set default element to style
37 37 this.$el.append(this.$slider_container);
38 38
39 39 // Set defaults.
40 40 this.update();
41 41 },
42 42
43 43 update : function(options){
44 44 // Update the contents of this view
45 45 //
46 46 // Called when the model is changed. The model may have been
47 47 // changed by another view or by a state update from the back-end.
48 48 if (options === undefined || options.updated_view != this) {
49 49 // JQuery slider option keys. These keys happen to have a
50 50 // one-to-one mapping with the corrosponding keys of the model.
51 51 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
52 var that = this;
52 53 _.each(jquery_slider_keys, function(key, i) {
53 var model_value = this.model.get(key);
54 var model_value = that.model.get(key);
54 55 if (model_value !== undefined) {
55 this.$slider.slider("option", key, model_value);
56 that.$slider.slider("option", key, model_value);
56 57 }
57 58 });
58 59
59 60 // WORKAROUND FOR JQUERY SLIDER BUG.
60 61 // The horizontal position of the slider handle
61 62 // depends on the value of the slider at the time
62 63 // of orientation change. Before applying the new
63 64 // workaround, we set the value to the minimum to
64 65 // make sure that the horizontal placement of the
65 66 // handle in the vertical slider is always
66 67 // consistent.
67 68 var orientation = this.model.get('orientation');
68 69 var value = this.model.get('min');
69 70 this.$slider.slider('option', 'value', value);
70 71 this.$slider.slider('option', 'orientation', orientation);
71 72 value = this.model.get('value');
72 73 this.$slider.slider('option', 'value', value);
73 74
74 75 // Use the right CSS classes for vertical & horizontal sliders
75 76 if (orientation=='vertical') {
76 77 this.$slider_container
77 78 .removeClass('widget-hslider')
78 79 .addClass('widget-vslider');
79 80 this.$el
80 81 .removeClass('widget-hbox-single')
81 82 .addClass('widget-vbox-single');
82 83 this.$label
83 84 .removeClass('widget-hlabel')
84 85 .addClass('widget-vlabel');
85 86
86 87 } else {
87 88 this.$slider_container
88 89 .removeClass('widget-vslider')
89 90 .addClass('widget-hslider');
90 91 this.$el
91 92 .removeClass('widget-vbox-single')
92 93 .addClass('widget-hbox-single');
93 94 this.$label
94 95 .removeClass('widget-vlabel')
95 96 .addClass('widget-hlabel');
96 97 }
97 98
98 99 var description = this.model.get('description');
99 100 if (description.length === 0) {
100 101 this.$label.hide();
101 102 } else {
102 103 this.$label.text(description);
103 104 this.$label.show();
104 105 }
105 106 }
106 107 return IntSliderView.__super__.update.apply(this);
107 108 },
108 109
109 110 events: {
110 111 // Dictionary of events and their handlers.
111 112 "slide" : "handleSliderChange"
112 113 },
113 114
114 115 handleSliderChange: function(e, ui) {
115 116 // Called when the slider value is changed.
116 117
117 118 // Calling model.set will trigger all of the other views of the
118 119 // model to update.
119 120 this.model.set('value', ~~ui.value, {updated_view: this}); // Double bit-wise not to truncate decimel
120 121 this.touch();
121 122 },
122 123 });
123 124 WidgetManager.register_widget_view('IntSliderView', IntSliderView);
124 125
125 126
126 127 var IntTextView = IPython.DOMWidgetView.extend({
127 128 render : function(){
128 129 // Called when view is rendered.
129 130 this.$el
130 131 .addClass('widget-hbox-single');
131 132 this.$label = $('<div />')
132 133 .appendTo(this.$el)
133 134 .addClass('widget-hlabel')
134 135 .hide();
135 136 this.$textbox = $('<input type="text" />')
136 137 .addClass('input')
137 138 .addClass('widget-numeric-text')
138 139 .appendTo(this.$el);
139 140 this.$el_to_style = this.$textbox; // Set default element to style
140 141 this.update(); // Set defaults.
141 142 },
142 143
143 144 update : function(options){
144 145 // Update the contents of this view
145 146 //
146 147 // Called when the model is changed. The model may have been
147 148 // changed by another view or by a state update from the back-end.
148 149 if (options === undefined || options.updated_view != this) {
149 150 var value = this.model.get('value');
150 151 if (parseInt(this.$textbox.val()) != value) {
151 152 this.$textbox.val(value);
152 153 }
153 154
154 155 if (this.model.get('disabled')) {
155 156 this.$textbox.attr('disabled','disabled');
156 157 } else {
157 158 this.$textbox.removeAttr('disabled');
158 159 }
159 160
160 161 var description = this.model.get('description');
161 162 if (description.length === 0) {
162 163 this.$label.hide();
163 164 } else {
164 165 this.$label.text(description);
165 166 this.$label.show();
166 167 }
167 168 }
168 169 return IntTextView.__super__.update.apply(this);
169 170 },
170 171
171 172 events: {
172 173 // Dictionary of events and their handlers.
173 174 "keyup input" : "handleChanging",
174 175 "paste input" : "handleChanging",
175 176 "cut input" : "handleChanging",
176 177
177 178 // Fires only when control is validated or looses focus.
178 179 "change input" : "handleChanged"
179 180 },
180 181
181 182 handleChanging: function(e) {
182 183 // Handles and validates user input.
183 184
184 185 // Try to parse value as a float.
185 186 var numericalValue = 0;
186 187 if (e.target.value !== '') {
187 188 numericalValue = parseInt(e.target.value);
188 189 }
189 190
190 191 // If parse failed, reset value to value stored in model.
191 192 if (isNaN(numericalValue)) {
192 193 e.target.value = this.model.get('value');
193 194 } else if (!isNaN(numericalValue)) {
194 195 if (this.model.get('max') !== undefined) {
195 196 numericalValue = Math.min(this.model.get('max'), numericalValue);
196 197 }
197 198 if (this.model.get('min') !== undefined) {
198 199 numericalValue = Math.max(this.model.get('min'), numericalValue);
199 200 }
200 201
201 202 // Apply the value if it has changed.
202 203 if (numericalValue != this.model.get('value')) {
203 204
204 205 // Calling model.set will trigger all of the other views of the
205 206 // model to update.
206 207 this.model.set('value', numericalValue, {updated_view: this});
207 208 this.touch();
208 209 }
209 210 }
210 211 },
211 212
212 213 handleChanged: function(e) {
213 214 // Applies validated input.
214 215 if (this.model.get('value') != e.target.value) {
215 216 e.target.value = this.model.get('value');
216 217 }
217 218 }
218 219 });
219 220 WidgetManager.register_widget_view('IntTextView', IntTextView);
220 221 });
@@ -1,372 +1,376 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // SelectionWidget
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["notebook/js/widgets/widget"], function(WidgetManager){
18 18
19 19 var DropdownView = IPython.DOMWidgetView.extend({
20 20 render : function(){
21 21 // Called when view is rendered.
22 22 this.$el
23 23 .addClass('widget-hbox-single');
24 24 this.$label = $('<div />')
25 25 .appendTo(this.$el)
26 26 .addClass('widget-hlabel')
27 27 .hide();
28 28 this.$buttongroup = $('<div />')
29 29 .addClass('widget_item')
30 30 .addClass('btn-group')
31 31 .appendTo(this.$el);
32 32 this.$el_to_style = this.$buttongroup; // Set default element to style
33 33 this.$droplabel = $('<button />')
34 34 .addClass('btn')
35 35 .addClass('widget-combo-btn')
36 36 .text(' ')
37 37 .appendTo(this.$buttongroup);
38 38 this.$dropbutton = $('<button />')
39 39 .addClass('btn')
40 40 .addClass('dropdown-toggle')
41 41 .addClass('widget-combo-carrot-btn')
42 42 .attr('data-toggle', 'dropdown')
43 43 .append($('<span />').addClass("caret"))
44 44 .appendTo(this.$buttongroup);
45 45 this.$droplist = $('<ul />')
46 46 .addClass('dropdown-menu')
47 47 .appendTo(this.$buttongroup);
48 48
49 49 // Set defaults.
50 50 this.update();
51 51 },
52 52
53 53 update : function(options){
54 54 // Update the contents of this view
55 55 //
56 56 // Called when the model is changed. The model may have been
57 57 // changed by another view or by a state update from the back-end.
58 58
59 59 if (options === undefined || options.updated_view != this) {
60 60 var selected_item_text = this.model.get('value');
61 61 if (selected_item_text.length === 0) {
62 62 this.$droplabel.text(' ');
63 63 } else {
64 64 this.$droplabel.text(selected_item_text);
65 65 }
66 66
67 67 var items = this.model.get('values');
68 68 var $replace_droplist = $('<ul />')
69 69 .addClass('dropdown-menu');
70 var that = this;
70 71 _.each(items, function(item, i) {
71 72 var item_button = $('<a href="#"/>')
72 73 .text(item)
73 .on('click', $.proxy(this.handle_click, this));
74 .on('click', $.proxy(that.handle_click, that));
74 75 $replace_droplist.append($('<li />').append(item_button));
75 76 });
76 77
77 78 this.$droplist.replaceWith($replace_droplist);
78 79 this.$droplist.remove();
79 80 this.$droplist = $replace_droplist;
80 81
81 82 if (this.model.get('disabled')) {
82 83 this.$buttongroup.attr('disabled','disabled');
83 84 this.$droplabel.attr('disabled','disabled');
84 85 this.$dropbutton.attr('disabled','disabled');
85 86 this.$droplist.attr('disabled','disabled');
86 87 } else {
87 88 this.$buttongroup.removeAttr('disabled');
88 89 this.$droplabel.removeAttr('disabled');
89 90 this.$dropbutton.removeAttr('disabled');
90 91 this.$droplist.removeAttr('disabled');
91 92 }
92 93
93 94 var description = this.model.get('description');
94 95 if (description.length === 0) {
95 96 this.$label.hide();
96 97 } else {
97 98 this.$label.text(description);
98 99 this.$label.show();
99 100 }
100 101 }
101 102 return DropdownView.__super__.update.apply(this);
102 103 },
103 104
104 105 handle_click: function (e) {
105 106 // Handle when a value is clicked.
106 107
107 108 // Calling model.set will trigger all of the other views of the
108 109 // model to update.
109 110 this.model.set('value', $(e.target).text(), {updated_view: this});
110 111 this.touch();
111 112 },
112 113
113 114 });
114 115 WidgetManager.register_widget_view('DropdownView', DropdownView);
115 116
116 117
117 118 var RadioButtonsView = IPython.DOMWidgetView.extend({
118 119 render : function(){
119 120 // Called when view is rendered.
120 121 this.$el
121 122 .addClass('widget-hbox');
122 123 this.$label = $('<div />')
123 124 .appendTo(this.$el)
124 125 .addClass('widget-hlabel')
125 126 .hide();
126 127 this.$container = $('<div />')
127 128 .appendTo(this.$el)
128 129 .addClass('widget-container')
129 130 .addClass('vbox');
130 131 this.$el_to_style = this.$container; // Set default element to style
131 132 this.update();
132 133 },
133 134
134 135 update : function(options){
135 136 // Update the contents of this view
136 137 //
137 138 // Called when the model is changed. The model may have been
138 139 // changed by another view or by a state update from the back-end.
139 140 if (options === undefined || options.updated_view != this) {
140 141 // Add missing items to the DOM.
141 142 var items = this.model.get('values');
142 143 var disabled = this.model.get('disabled');
144 var that = this;
143 145 _.each(items, function(item, index) {
144 146 var item_query = ' :input[value="' + item + '"]';
145 if (this.$el.find(item_query).length === 0) {
147 if (that.$el.find(item_query).length === 0) {
146 148 var $label = $('<label />')
147 149 .addClass('radio')
148 150 .text(item)
149 .appendTo(this.$container);
151 .appendTo(that.$container);
150 152
151 153 $('<input />')
152 154 .attr('type', 'radio')
153 .addClass(this.model)
155 .addClass(that.model)
154 156 .val(item)
155 157 .prependTo($label)
156 .on('click', $.proxy(this.handle_click, this));
158 .on('click', $.proxy(that.handle_click, that));
157 159 }
158 160
159 161 var $item_element = this.$container.find(item_query);
160 162 if (this.model.get('value') == item) {
161 163 $item_element.prop('checked', true);
162 164 } else {
163 165 $item_element.prop('checked', false);
164 166 }
165 167 $item_element.prop('disabled', disabled);
166 168 });
167 169
168 170 // Remove items that no longer exist.
169 171 this.$container.find('input').each(function(i, obj) {
170 172 var value = $(obj).val();
171 173 var found = false;
172 174 _.each(items, function(item, index) {
173 175 if (item == value) {
174 176 found = true;
175 177 return false;
176 178 }
177 179 });
178 180
179 181 if (!found) {
180 182 $(obj).parent().remove();
181 183 }
182 184 });
183 185
184 186 var description = this.model.get('description');
185 187 if (description.length === 0) {
186 188 this.$label.hide();
187 189 } else {
188 190 this.$label.text(description);
189 191 this.$label.show();
190 192 }
191 193 }
192 194 return RadioButtonsView.__super__.update.apply(this);
193 195 },
194 196
195 197 handle_click: function (e) {
196 198 // Handle when a value is clicked.
197 199
198 200 // Calling model.set will trigger all of the other views of the
199 201 // model to update.
200 202 this.model.set('value', $(e.target).val(), {updated_view: this});
201 203 this.touch();
202 204 },
203 205 });
204 206 WidgetManager.register_widget_view('RadioButtonsView', RadioButtonsView);
205 207
206 208
207 209 var ToggleButtonsView = IPython.DOMWidgetView.extend({
208 210 render : function(){
209 211 // Called when view is rendered.
210 212 this.$el
211 213 .addClass('widget-hbox-single');
212 214 this.$label = $('<div />')
213 215 .appendTo(this.$el)
214 216 .addClass('widget-hlabel')
215 217 .hide();
216 218 this.$buttongroup = $('<div />')
217 219 .addClass('btn-group')
218 220 .attr('data-toggle', 'buttons-radio')
219 221 .appendTo(this.$el);
220 222 this.$el_to_style = this.$buttongroup; // Set default element to style
221 223 this.update();
222 224 },
223 225
224 226 update : function(options){
225 227 // Update the contents of this view
226 228 //
227 229 // Called when the model is changed. The model may have been
228 230 // changed by another view or by a state update from the back-end.
229 231 if (options === undefined || options.updated_view != this) {
230 232 // Add missing items to the DOM.
231 233 var items = this.model.get('values');
232 234 var disabled = this.model.get('disabled');
235 var that = this;
233 236 _.each(items, function(item, index) {
234 237 var item_query = ' :contains("' + item + '")';
235 if (this.$buttongroup.find(item_query).length === 0) {
238 if (that.$buttongroup.find(item_query).length === 0) {
236 239 $('<button />')
237 240 .attr('type', 'button')
238 241 .addClass('btn')
239 242 .text(item)
240 .appendTo(this.$buttongroup)
241 .on('click', $.proxy(this.handle_click, this));
243 .appendTo(that.$buttongroup)
244 .on('click', $.proxy(that.handle_click, that));
242 245 }
243 246
244 247 var $item_element = this.$buttongroup.find(item_query);
245 248 if (this.model.get('value') == item) {
246 249 $item_element.addClass('active');
247 250 } else {
248 251 $item_element.removeClass('active');
249 252 }
250 253 $item_element.prop('disabled', disabled);
251 254 });
252 255
253 256 // Remove items that no longer exist.
254 257 this.$buttongroup.find('button').each(function(i, obj) {
255 258 var value = $(obj).text();
256 259 var found = false;
257 260 _.each(items, function(item, index) {
258 261 if (item == value) {
259 262 found = true;
260 263 return false;
261 264 }
262 265 });
263 266
264 267 if (!found) {
265 268 $(obj).remove();
266 269 }
267 270 });
268 271
269 272 var description = this.model.get('description');
270 273 if (description.length === 0) {
271 274 this.$label.hide();
272 275 } else {
273 276 this.$label.text(description);
274 277 this.$label.show();
275 278 }
276 279 }
277 280 return ToggleButtonsView.__super__.update.apply(this);
278 281 },
279 282
280 283 handle_click: function (e) {
281 284 // Handle when a value is clicked.
282 285
283 286 // Calling model.set will trigger all of the other views of the
284 287 // model to update.
285 288 this.model.set('value', $(e.target).text(), {updated_view: this});
286 289 this.touch();
287 290 },
288 291 });
289 292 WidgetManager.register_widget_view('ToggleButtonsView', ToggleButtonsView);
290 293
291 294
292 295 var ListBoxView = IPython.DOMWidgetView.extend({
293 296 render : function(){
294 297 // Called when view is rendered.
295 298 this.$el
296 299 .addClass('widget-hbox');
297 300 this.$label = $('<div />')
298 301 .appendTo(this.$el)
299 302 .addClass('widget-hlabel')
300 303 .hide();
301 304 this.$listbox = $('<select />')
302 305 .addClass('widget-listbox')
303 306 .attr('size', 6)
304 307 .appendTo(this.$el);
305 308 this.$el_to_style = this.$listbox; // Set default element to style
306 309 this.update();
307 310 },
308 311
309 312 update : function(options){
310 313 // Update the contents of this view
311 314 //
312 315 // Called when the model is changed. The model may have been
313 316 // changed by another view or by a state update from the back-end.
314 317 if (options === undefined || options.updated_view != this) {
315 318 // Add missing items to the DOM.
316 319 var items = this.model.get('values');
320 var that = this;
317 321 _.each(items, function(item, index) {
318 322 var item_query = ' :contains("' + item + '")';
319 if (this.$listbox.find(item_query).length === 0) {
323 if (that.$listbox.find(item_query).length === 0) {
320 324 $('<option />')
321 325 .text(item)
322 326 .attr('value', item)
323 .appendTo(this.$listbox)
324 .on('click', $.proxy(this.handle_click, this));
327 .appendTo(that.$listbox)
328 .on('click', $.proxy(that.handle_click, that));
325 329 }
326 330 });
327 331
328 332 // Select the correct element
329 333 this.$listbox.val(this.model.get('value'));
330 334
331 335 // Disable listbox if needed
332 336 var disabled = this.model.get('disabled');
333 337 this.$listbox.prop('disabled', disabled);
334 338
335 339 // Remove items that no longer exist.
336 340 this.$listbox.find('option').each(function(i, obj) {
337 341 var value = $(obj).text();
338 342 var found = false;
339 343 _.each(items, function(item, index) {
340 344 if (item == value) {
341 345 found = true;
342 346 return false;
343 347 }
344 348 });
345 349
346 350 if (!found) {
347 351 $(obj).remove();
348 352 }
349 353 });
350 354
351 355 var description = this.model.get('description');
352 356 if (description.length === 0) {
353 357 this.$label.hide();
354 358 } else {
355 359 this.$label.text(description);
356 360 this.$label.show();
357 361 }
358 362 }
359 363 return ListBoxView.__super__.update.apply(this);
360 364 },
361 365
362 366 handle_click: function (e) {
363 367 // Handle when a value is clicked.
364 368
365 369 // Calling model.set will trigger all of the other views of the
366 370 // model to update.
367 371 this.model.set('value', $(e.target).text(), {updated_view: this});
368 372 this.touch();
369 373 },
370 374 });
371 375 WidgetManager.register_widget_view('ListBoxView', ListBoxView);
372 376 });
@@ -1,242 +1,244 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // SelectionContainerWidget
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["notebook/js/widgets/widget"], function(WidgetManager){
18 18
19 19 var AccordionView = IPython.DOMWidgetView.extend({
20 20 render: function(){
21 21 // Called when view is rendered.
22 22 var guid = 'accordion' + IPython.utils.uuid();
23 23 this.$el
24 24 .attr('id', guid)
25 25 .addClass('accordion');
26 26 this.containers = [];
27 27 this.model_containers = {};
28 28 this.update_children([], this.model.get('_children'));
29 29 this.model.on('change:_children', function(model, value, options) {
30 30 this.update_children(model.previous('_children'), value);
31 31 }, this);
32 32 },
33 33
34 34 update: function(options) {
35 35 // Update the contents of this view
36 36 //
37 37 // Called when the model is changed. The model may have been
38 38 // changed by another view or by a state update from the back-end.
39 39 if (options === undefined || options.updated_view != this) {
40 40 // Set tab titles
41 41 var titles = this.model.get('_titles');
42 var that = this;
42 43 _.each(titles, function(title, page_index) {
43 var accordian = this.containers[page_index];
44 var accordian = that.containers[page_index];
44 45 if (accordian !== undefined) {
45 46 accordian
46 47 .find('.accordion-heading')
47 48 .find('.accordion-toggle')
48 49 .text(title);
49 50 }
50 51 });
51 52
52 53 // Set selected page
53 54 var selected_index = this.model.get("selected_index");
54 55 if (0 <= selected_index && selected_index < this.containers.length) {
55 56 _.each(this.containers, function(container, index) {
56 57 if (index==selected_index) {
57 58 container.find('.accordion-body').collapse('show');
58 59 } else {
59 60 container.find('.accordion-body').collapse('hide');
60 61 }
61 62 });
62 63 }
63 64 }
64 65 return AccordionView.__super__.update.apply(this);
65 66 },
66 67
67 68 update_children: function(old_list, new_list) {
68 69 // Called when the children list is modified.
69 70 this.do_diff(old_list,
70 71 new_list,
71 72 $.proxy(this.remove_child_model, this),
72 73 $.proxy(this.add_child_model, this));
73 74 },
74 75
75 76 remove_child_model: function(model) {
76 77 // Called when a child is removed from children list.
77 78 var accordion_group = this.model_containers[model.id];
78 79 this.containers.splice(accordion_group.container_index, 1);
79 80 delete this.model_containers[model.id];
80 81 accordion_group.remove();
81 82 this.delete_child_view(model);
82 83 },
83 84
84 85 add_child_model: function(model) {
85 86 // Called when a child is added to children list.
86 87 var view = this.create_child_view(model);
87 88 var index = this.containers.length;
88 89 var uuid = IPython.utils.uuid();
89 90 var accordion_group = $('<div />')
90 91 .addClass('accordion-group')
91 92 .appendTo(this.$el);
92 93 var accordion_heading = $('<div />')
93 94 .addClass('accordion-heading')
94 95 .appendTo(accordion_group);
95 96 var that = this;
96 97 var accordion_toggle = $('<a />')
97 98 .addClass('accordion-toggle')
98 99 .attr('data-toggle', 'collapse')
99 100 .attr('data-parent', '#' + this.$el.attr('id'))
100 101 .attr('href', '#' + uuid)
101 102 .click(function(evt){
102 103
103 104 // Calling model.set will trigger all of the other views of the
104 105 // model to update.
105 106 that.model.set("selected_index", index, {updated_view: this});
106 107 that.touch();
107 108 })
108 109 .text('Page ' + index)
109 110 .appendTo(accordion_heading);
110 111 var accordion_body = $('<div />', {id: uuid})
111 112 .addClass('accordion-body collapse')
112 113 .appendTo(accordion_group);
113 114 var accordion_inner = $('<div />')
114 115 .addClass('accordion-inner')
115 116 .appendTo(accordion_body);
116 117 var container_index = this.containers.push(accordion_group) - 1;
117 118 accordion_group.container_index = container_index;
118 119 this.model_containers[model.id] = accordion_group;
119 120 accordion_inner.append(view.$el);
120 121
121 122 this.update();
122 123
123 124 // Stupid workaround to close the bootstrap accordion tabs which
124 125 // open by default even though they don't have the `in` class
125 126 // attached to them. For some reason a delay is required.
126 127 // TODO: Better fix.
127 128 setTimeout(function(){ that.update(); }, 500);
128 129 },
129 130 });
130 131 WidgetManager.register_widget_view('AccordionView', AccordionView);
131 132
132 133
133 134 var TabView = IPython.DOMWidgetView.extend({
134 135 initialize: function() {
135 136 // Public constructor.
136 137 this.containers = [];
137 138 TabView.__super__.initialize.apply(this, arguments);
138 139 },
139 140
140 141 render: function(){
141 142 // Called when view is rendered.
142 143 var uuid = 'tabs'+IPython.utils.uuid();
143 144 var that = this;
144 145 this.$tabs = $('<div />', {id: uuid})
145 146 .addClass('nav')
146 147 .addClass('nav-tabs')
147 148 .appendTo(this.$el);
148 149 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
149 150 .addClass('tab-content')
150 151 .appendTo(this.$el);
151 152 this.containers = [];
152 153 this.update_children([], this.model.get('_children'));
153 154 this.model.on('change:_children', function(model, value, options) {
154 155 this.update_children(model.previous('_children'), value);
155 156 }, this);
156 157 },
157 158
158 159 update_children: function(old_list, new_list) {
159 160 // Called when the children list is modified.
160 161 this.do_diff(old_list,
161 162 new_list,
162 163 $.proxy(this.remove_child_model, this),
163 164 $.proxy(this.add_child_model, this));
164 165 },
165 166
166 167 remove_child_model: function(model) {
167 168 // Called when a child is removed from children list.
168 169 var view = this.child_views[model.id];
169 170 this.containers.splice(view.parent_tab.tab_text_index, 1);
170 171 view.parent_tab.remove();
171 172 view.parent_container.remove();
172 173 view.remove();
173 174 this.delete_child_view(model);
174 175 },
175 176
176 177 add_child_model: function(model) {
177 178 // Called when a child is added to children list.
178 179 var view = this.create_child_view(model);
179 180 var index = this.containers.length;
180 181 var uuid = IPython.utils.uuid();
181 182
182 183 var that = this;
183 184 var tab = $('<li />')
184 185 .css('list-style-type', 'none')
185 186 .appendTo(this.$tabs);
186 187 view.parent_tab = tab;
187 188
188 189 var tab_text = $('<a />')
189 190 .attr('href', '#' + uuid)
190 191 .attr('data-toggle', 'tab')
191 192 .text('Page ' + index)
192 193 .appendTo(tab)
193 194 .click(function (e) {
194 195
195 196 // Calling model.set will trigger all of the other views of the
196 197 // model to update.
197 198 that.model.set("selected_index", index, {updated_view: this});
198 199 that.touch();
199 200 that.select_page(index);
200 201 });
201 202 tab.tab_text_index = this.containers.push(tab_text) - 1;
202 203
203 204 var contents_div = $('<div />', {id: uuid})
204 205 .addClass('tab-pane')
205 206 .addClass('fade')
206 207 .append(view.$el)
207 208 .appendTo(this.$tab_contents);
208 209 view.parent_container = contents_div;
209 210 },
210 211
211 212 update: function(options) {
212 213 // Update the contents of this view
213 214 //
214 215 // Called when the model is changed. The model may have been
215 216 // changed by another view or by a state update from the back-end.
216 217 if (options === undefined || options.updated_view != this) {
217 218 // Set tab titles
218 219 var titles = this.model.get('_titles');
220 var that = this;
219 221 _.each(titles, function(title, page_index) {
220 var tab_text = this.containers[page_index];
222 var tab_text = that.containers[page_index];
221 223 if (tab_text !== undefined) {
222 224 tab_text.text(title);
223 225 }
224 226 });
225 227
226 228 var selected_index = this.model.get('selected_index');
227 229 if (0 <= selected_index && selected_index < this.containers.length) {
228 230 this.select_page(selected_index);
229 231 }
230 232 }
231 233 return TabView.__super__.update.apply(this);
232 234 },
233 235
234 236 select_page: function(index) {
235 237 // Select a page.
236 238 this.$tabs.find('li')
237 239 .removeClass('active');
238 240 this.containers[index].tab('show');
239 241 },
240 242 });
241 243 WidgetManager.register_widget_view('TabView', TabView);
242 244 });
@@ -1,478 +1,483 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 import inspect
17 17 import types
18 18
19 19 from IPython.kernel.comm import Comm
20 20 from IPython.config import LoggingConfigurable
21 21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List
22 22 from IPython.utils.py3compat import string_types
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Classes
26 26 #-----------------------------------------------------------------------------
27 27 class CallbackDispatcher(LoggingConfigurable):
28 28 acceptable_nargs = List([], help="""List of integers.
29 29 The number of arguments in the callbacks registered must match one of
30 30 the integers in this list. If this list is empty or None, it will be
31 31 ignored.""")
32 32
33 33 def __init__(self, *pargs, **kwargs):
34 34 """Constructor"""
35 35 LoggingConfigurable.__init__(self, *pargs, **kwargs)
36 36 self.callbacks = {}
37 37
38 38 def __call__(self, *pargs, **kwargs):
39 39 """Call all of the registered callbacks that have the same number of
40 40 positional arguments."""
41 41 nargs = len(pargs)
42 42 self._validate_nargs(nargs)
43 43 if nargs in self.callbacks:
44 44 for callback in self.callbacks[nargs]:
45 45 callback(*pargs, **kwargs)
46 46
47 47 def register_callback(self, callback, remove=False):
48 48 """(Un)Register a callback
49 49
50 50 Parameters
51 51 ----------
52 52 callback: method handle
53 53 Method to be registered or unregisted.
54 54 remove=False: bool
55 55 Whether or not to unregister the callback."""
56 56
57 57 # Validate the number of arguments that the callback accepts.
58 58 nargs = self._get_nargs(callback)
59 59 self._validate_nargs(nargs)
60 60
61 61 # Get/create the appropriate list of callbacks.
62 62 if nargs not in self.callbacks:
63 63 self.callbacks[nargs] = []
64 64 callback_list = self.callbacks[nargs]
65 65
66 66 # (Un)Register the callback.
67 67 if remove and callback in callback_list:
68 68 callback_list.remove(callback)
69 69 elif not remove and callback not in callback_list:
70 70 callback_list.append(callback)
71 71
72 72 def _validate_nargs(self, nargs):
73 73 if self.acceptable_nargs is not None and \
74 74 len(self.acceptable_nargs) > 0 and \
75 75 nargs not in self.acceptable_nargs:
76 76
77 77 raise TypeError('Invalid number of positional arguments. See acceptable_nargs list.')
78 78
79 79 def _get_nargs(self, callback):
80 80 """Gets the number of arguments in a callback"""
81 81 if callable(callback):
82 82 argspec = inspect.getargspec(callback)
83 nargs = len(argspec[1]) # Only count vargs!
83 if argspec[0] is None:
84 nargs = 0
85 elif argspec[3] is None:
86 nargs = len(argspec[0]) # Only count vargs!
87 else:
88 nargs = len(argspec[0]) - len(argspec[3]) # Subtract number of defaults.
84 89
85 90 # Bound methods have an additional 'self' argument
86 91 if isinstance(callback, types.MethodType):
87 92 nargs -= 1
88 93 return nargs
89 94 else:
90 95 raise TypeError('Callback must be callable.')
91 96
92 97
93 98 class Widget(LoggingConfigurable):
94 99 #-------------------------------------------------------------------------
95 100 # Class attributes
96 101 #-------------------------------------------------------------------------
97 102 widget_construction_callback = None
98 103 widgets = {}
99 104
100 105 def on_widget_constructed(callback):
101 106 """Registers a callback to be called when a widget is constructed.
102 107
103 108 The callback must have the following signature:
104 109 callback(widget)"""
105 110 Widget.widget_construction_callback = callback
106 111
107 112 def _call_widget_constructed(widget):
108 113 """Class method, called when a widget is constructed."""
109 114 if Widget.widget_construction_callback is not None and callable(Widget.widget_construction_callback):
110 115 Widget.widget_construction_callback(widget)
111 116
112 117 #-------------------------------------------------------------------------
113 118 # Traits
114 119 #-------------------------------------------------------------------------
115 120 model_name = Unicode('WidgetModel', help="""Name of the backbone model
116 121 registered in the front-end to create and sync this widget with.""")
117 122 view_name = Unicode(help="""Default view registered in the front-end
118 123 to use to represent the widget.""", sync=True)
119 124 _comm = Instance('IPython.kernel.comm.Comm')
120 125
121 126 #-------------------------------------------------------------------------
122 127 # (Con/de)structor
123 128 #-------------------------------------------------------------------------
124 129 def __init__(self, **kwargs):
125 130 """Public constructor"""
126 131 self.closed = False
127 132
128 133 self._property_lock = (None, None)
129 134 self._keys = None
130 135
131 136 self._display_callbacks = CallbackDispatcher(acceptable_nargs=[0])
132 137 self._msg_callbacks = CallbackDispatcher(acceptable_nargs=[1, 2])
133 138
134 139 super(Widget, self).__init__(**kwargs)
135 140
136 141 self.on_trait_change(self._handle_property_changed, self.keys)
137 142 Widget._call_widget_constructed(self)
138 143
139 144 def __del__(self):
140 145 """Object disposal"""
141 146 self.close()
142 147
143 148 #-------------------------------------------------------------------------
144 149 # Properties
145 150 #-------------------------------------------------------------------------
146 151 @property
147 152 def keys(self):
148 153 """Gets a list of the traitlets that should be synced with the front-end."""
149 154 if self._keys is None:
150 155 self._keys = []
151 156 for trait_name in self.trait_names():
152 157 if self.trait_metadata(trait_name, 'sync'):
153 158 self._keys.append(trait_name)
154 159 return self._keys
155 160
156 161 @property
157 162 def comm(self):
158 163 """Gets the Comm associated with this widget.
159 164
160 165 If a Comm doesn't exist yet, a Comm will be created automagically."""
161 166 if self._comm is None:
162 167 # Create a comm.
163 168 self._comm = Comm(target_name=self.model_name)
164 169 self._comm.on_msg(self._handle_msg)
165 170 self._comm.on_close(self._close)
166 171 Widget.widgets[self.model_id] = self
167 172
168 173 # first update
169 174 self.send_state()
170 175 return self._comm
171 176
172 177 @property
173 178 def model_id(self):
174 179 """Gets the model id of this widget.
175 180
176 181 If a Comm doesn't exist yet, a Comm will be created automagically."""
177 182 return self.comm.comm_id
178 183
179 184 #-------------------------------------------------------------------------
180 185 # Methods
181 186 #-------------------------------------------------------------------------
182 187 def close(self):
183 188 """Close method.
184 189
185 190 Closes the widget which closes the underlying comm.
186 191 When the comm is closed, all of the widget views are automatically
187 192 removed from the front-end."""
188 193 if not self.closed:
189 194 self._comm.close()
190 195 self._close()
191 196
192 197 def send_state(self, key=None):
193 198 """Sends the widget state, or a piece of it, to the front-end.
194 199
195 200 Parameters
196 201 ----------
197 202 key : unicode (optional)
198 203 A single property's name to sync with the front-end.
199 204 """
200 205 self._send({
201 206 "method" : "update",
202 207 "state" : self.get_state()
203 208 })
204 209
205 210 def get_state(self, key=None):
206 211 """Gets the widget state, or a piece of it.
207 212
208 213 Parameters
209 214 ----------
210 215 key : unicode (optional)
211 216 A single property's name to get.
212 217 """
213 218 keys = self.keys if key is None else [key]
214 219 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
215 220
216 221 def send(self, content):
217 222 """Sends a custom msg to the widget model in the front-end.
218 223
219 224 Parameters
220 225 ----------
221 226 content : dict
222 227 Content of the message to send.
223 228 """
224 229 self._send({"method": "custom", "content": content})
225 230
226 231 def on_msg(self, callback, remove=False):
227 232 """(Un)Register a custom msg recieve callback.
228 233
229 234 Parameters
230 235 ----------
231 236 callback: method handler
232 237 Can have a signature of:
233 238 - callback(content) Signature 1
234 239 - callback(sender, content) Signature 2
235 240 remove: bool
236 241 True if the callback should be unregistered."""
237 242 self._msg_callbacks.register_callback(callback, remove=remove)
238 243
239 244 def on_displayed(self, callback, remove=False):
240 245 """(Un)Register a widget displayed callback.
241 246
242 247 Parameters
243 248 ----------
244 249 callback: method handler
245 250 Can have a signature of:
246 251 - callback(sender, **kwargs)
247 252 kwargs from display call passed through without modification.
248 253 remove: bool
249 254 True if the callback should be unregistered."""
250 255 self._display_callbacks.register_callback(callback, remove=remove)
251 256
252 257 #-------------------------------------------------------------------------
253 258 # Support methods
254 259 #-------------------------------------------------------------------------
255 260 @contextmanager
256 261 def _lock_property(self, key, value):
257 262 """Lock a property-value pair.
258 263
259 264 NOTE: This, in addition to the single lock for all state changes, is
260 265 flawed. In the future we may want to look into buffering state changes
261 266 back to the front-end."""
262 267 self._property_lock = (key, value)
263 268 try:
264 269 yield
265 270 finally:
266 271 self._property_lock = (None, None)
267 272
268 273 def _should_send_property(self, key, value):
269 274 """Check the property lock (property_lock)"""
270 275 return key != self._property_lock[0] or \
271 276 value != self._property_lock[1]
272 277
273 278 def _close(self):
274 279 """Unsafe close"""
275 280 del Widget.widgets[self.model_id]
276 281 self._comm = None
277 282 self.closed = True
278 283
279 284 # Event handlers
280 285 def _handle_msg(self, msg):
281 286 """Called when a msg is received from the front-end"""
282 287 data = msg['content']['data']
283 288 method = data['method']
284 289 if not method in ['backbone', 'custom']:
285 290 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
286 291
287 292 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
288 293 if method == 'backbone' and 'sync_data' in data:
289 294 sync_data = data['sync_data']
290 295 self._handle_receive_state(sync_data) # handles all methods
291 296
292 297 # Handle a custom msg from the front-end
293 298 elif method == 'custom':
294 299 if 'content' in data:
295 300 self._handle_custom_msg(data['content'])
296 301
297 302 def _handle_receive_state(self, sync_data):
298 303 """Called when a state is received from the front-end."""
299 304 for name in self.keys:
300 305 if name in sync_data:
301 306 value = self._unpack_widgets(sync_data[name])
302 307 with self._lock_property(name, value):
303 308 setattr(self, name, value)
304 309
305 310 def _handle_custom_msg(self, content):
306 311 """Called when a custom msg is received."""
307 312 self._msg_callbacks(content) # Signature 1
308 313 self._msg_callbacks(self, content) # Signature 2
309 314
310 315 def _handle_property_changed(self, name, old, new):
311 316 """Called when a property has been changed."""
312 317 # Make sure this isn't information that the front-end just sent us.
313 318 if self._should_send_property(name, new):
314 319 # Send new state to front-end
315 320 self.send_state(key=name)
316 321
317 322 def _handle_displayed(self, **kwargs):
318 323 """Called when a view has been displayed for this widget instance"""
319 324 self._display_callbacks(**kwargs)
320 325
321 326 def _pack_widgets(self, x):
322 327 """Recursively converts all widget instances to model id strings.
323 328
324 329 Children widgets will be stored and transmitted to the front-end by
325 330 their model ids. Return value must be JSON-able."""
326 331 if isinstance(x, dict):
327 332 return {k: self._pack_widgets(v) for k, v in x.items()}
328 333 elif isinstance(x, list):
329 334 return [self._pack_widgets(v) for v in x]
330 335 elif isinstance(x, Widget):
331 336 return x.model_id
332 337 else:
333 338 return x # Value must be JSON-able
334 339
335 340 def _unpack_widgets(self, x):
336 341 """Recursively converts all model id strings to widget instances.
337 342
338 343 Children widgets will be stored and transmitted to the front-end by
339 344 their model ids."""
340 345 if isinstance(x, dict):
341 346 return {k: self._unpack_widgets(v) for k, v in x.items()}
342 347 elif isinstance(x, list):
343 348 return [self._unpack_widgets(v) for v in x]
344 349 elif isinstance(x, string_types):
345 350 return x if x not in Widget.widgets else Widget.widgets[x]
346 351 else:
347 352 return x
348 353
349 354 def _ipython_display_(self, **kwargs):
350 355 """Called when `IPython.display.display` is called on the widget."""
351 356 # Show view. By sending a display message, the comm is opened and the
352 357 # initial state is sent.
353 358 self._send({"method": "display"})
354 359 self._handle_displayed(**kwargs)
355 360
356 361 def _send(self, msg):
357 362 """Sends a message to the model in the front-end."""
358 363 self.comm.send(msg)
359 364
360 365
361 366 class DOMWidget(Widget):
362 367 visible = Bool(True, help="Whether or not the widget is visible.", sync=True)
363 368 _css = Dict(sync=True) # Internal CSS property dict
364 369
365 370 def get_css(self, key, selector=""):
366 371 """Get a CSS property of the widget.
367 372
368 373 Note: This function does not actually request the CSS from the
369 374 front-end; Only properties that have been set with set_css can be read.
370 375
371 376 Parameters
372 377 ----------
373 378 key: unicode
374 379 CSS key
375 380 selector: unicode (optional)
376 381 JQuery selector used when the CSS key/value was set.
377 382 """
378 383 if selector in self._css and key in self._css[selector]:
379 384 return self._css[selector][key]
380 385 else:
381 386 return None
382 387
383 388 def set_css(self, *args, **kwargs):
384 389 """Set one or more CSS properties of the widget.
385 390
386 391 This function has two signatures:
387 392 - set_css(css_dict, selector='')
388 393 - set_css(key, value, selector='')
389 394
390 395 Parameters
391 396 ----------
392 397 css_dict : dict
393 398 CSS key/value pairs to apply
394 399 key: unicode
395 400 CSS key
396 401 value
397 402 CSS value
398 403 selector: unicode (optional)
399 404 JQuery selector to use to apply the CSS key/value. If no selector
400 405 is provided, an empty selector is used. An empty selector makes the
401 406 front-end try to apply the css to a default element. The default
402 407 element is an attribute unique to each view, which is a DOM element
403 408 of the view that should be styled with common CSS (see
404 409 `$el_to_style` in the Javascript code).
405 410 """
406 411 selector = kwargs.get('selector', '')
407 412 if not selector in self._css:
408 413 self._css[selector] = {}
409 414
410 415 # Signature 1: set_css(css_dict, selector='')
411 416 if len(args) == 1:
412 417 if isinstance(args[0], dict):
413 418 for (key, value) in args[0].items():
414 419 if not (key in self._css[selector] and value == self._css[selector][key]):
415 420 self._css[selector][key] = value
416 421 self.send_state('_css')
417 422 else:
418 423 raise Exception('css_dict must be a dict.')
419 424
420 425 # Signature 2: set_css(key, value, selector='')
421 426 elif len(args) == 2 or len(args) == 3:
422 427
423 428 # Selector can be a positional arg if it's the 3rd value
424 429 if len(args) == 3:
425 430 selector = args[2]
426 431 if selector not in self._css:
427 432 self._css[selector] = {}
428 433
429 434 # Only update the property if it has changed.
430 435 key = args[0]
431 436 value = args[1]
432 437 if not (key in self._css[selector] and value == self._css[selector][key]):
433 438 self._css[selector][key] = value
434 439 self.send_state('_css') # Send new state to client.
435 440 else:
436 441 raise Exception('set_css only accepts 1-3 arguments')
437 442
438 443 def add_class(self, class_names, selector=""):
439 444 """Add class[es] to a DOM element.
440 445
441 446 Parameters
442 447 ----------
443 448 class_names: unicode or list
444 449 Class name(s) to add to the DOM element(s).
445 450 selector: unicode (optional)
446 451 JQuery selector to select the DOM element(s) that the class(es) will
447 452 be added to.
448 453 """
449 454 class_list = class_names
450 455 if isinstance(class_list, list):
451 456 class_list = ' '.join(class_list)
452 457
453 458 self.send({
454 459 "msg_type" : "add_class",
455 460 "class_list" : class_list,
456 461 "selector" : selector
457 462 })
458 463
459 464 def remove_class(self, class_names, selector=""):
460 465 """Remove class[es] from a DOM element.
461 466
462 467 Parameters
463 468 ----------
464 469 class_names: unicode or list
465 470 Class name(s) to remove from the DOM element(s).
466 471 selector: unicode (optional)
467 472 JQuery selector to select the DOM element(s) that the class(es) will
468 473 be removed from.
469 474 """
470 475 class_list = class_names
471 476 if isinstance(class_list, list):
472 477 class_list = ' '.join(class_list)
473 478
474 479 self.send({
475 480 "msg_type" : "remove_class",
476 481 "class_list" : class_list,
477 482 "selector" : selector,
478 483 })
General Comments 0
You need to be logged in to leave comments. Login now