##// END OF EJS Templates
Merge pull request #5170 from jdfreder/widget-throttle-trait...
Min RK -
r15424:f6ccfaf8 merge
parent child Browse files
Show More
@@ -1,451 +1,450
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Base Widget Model and View classes
9 // Base Widget Model and View classes
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["notebook/js/widgetmanager",
17 define(["notebook/js/widgetmanager",
18 "underscore",
18 "underscore",
19 "backbone"],
19 "backbone"],
20 function(WidgetManager, _, Backbone){
20 function(WidgetManager, _, Backbone){
21
21
22 var WidgetModel = Backbone.Model.extend({
22 var WidgetModel = Backbone.Model.extend({
23 constructor: function (widget_manager, model_id, comm) {
23 constructor: function (widget_manager, model_id, comm) {
24 // Constructor
24 // Constructor
25 //
25 //
26 // Creates a WidgetModel instance.
26 // Creates a WidgetModel instance.
27 //
27 //
28 // Parameters
28 // Parameters
29 // ----------
29 // ----------
30 // widget_manager : WidgetManager instance
30 // widget_manager : WidgetManager instance
31 // model_id : string
31 // model_id : string
32 // An ID unique to this model.
32 // An ID unique to this model.
33 // comm : Comm instance (optional)
33 // comm : Comm instance (optional)
34 this.widget_manager = widget_manager;
34 this.widget_manager = widget_manager;
35 this._buffered_state_diff = {};
35 this._buffered_state_diff = {};
36 this.pending_msgs = 0;
36 this.pending_msgs = 0;
37 this.msg_throttle = 3;
38 this.msg_buffer = null;
37 this.msg_buffer = null;
39 this.key_value_lock = null;
38 this.key_value_lock = null;
40 this.id = model_id;
39 this.id = model_id;
41 this.views = [];
40 this.views = [];
42
41
43 if (comm !== undefined) {
42 if (comm !== undefined) {
44 // Remember comm associated with the model.
43 // Remember comm associated with the model.
45 this.comm = comm;
44 this.comm = comm;
46 comm.model = this;
45 comm.model = this;
47
46
48 // Hook comm messages up to model.
47 // Hook comm messages up to model.
49 comm.on_close($.proxy(this._handle_comm_closed, this));
48 comm.on_close($.proxy(this._handle_comm_closed, this));
50 comm.on_msg($.proxy(this._handle_comm_msg, this));
49 comm.on_msg($.proxy(this._handle_comm_msg, this));
51 }
50 }
52 return Backbone.Model.apply(this);
51 return Backbone.Model.apply(this);
53 },
52 },
54
53
55 send: function (content, callbacks) {
54 send: function (content, callbacks) {
56 // Send a custom msg over the comm.
55 // Send a custom msg over the comm.
57 if (this.comm !== undefined) {
56 if (this.comm !== undefined) {
58 var data = {method: 'custom', content: content};
57 var data = {method: 'custom', content: content};
59 this.comm.send(data, callbacks);
58 this.comm.send(data, callbacks);
60 this.pending_msgs++;
59 this.pending_msgs++;
61 }
60 }
62 },
61 },
63
62
64 _handle_comm_closed: function (msg) {
63 _handle_comm_closed: function (msg) {
65 // Handle when a widget is closed.
64 // Handle when a widget is closed.
66 this.trigger('comm:close');
65 this.trigger('comm:close');
67 delete this.comm.model; // Delete ref so GC will collect widget model.
66 delete this.comm.model; // Delete ref so GC will collect widget model.
68 delete this.comm;
67 delete this.comm;
69 delete this.model_id; // Delete id from model so widget manager cleans up.
68 delete this.model_id; // Delete id from model so widget manager cleans up.
70 _.each(this.views, function(view, i) {
69 _.each(this.views, function(view, i) {
71 view.remove();
70 view.remove();
72 });
71 });
73 },
72 },
74
73
75 _handle_comm_msg: function (msg) {
74 _handle_comm_msg: function (msg) {
76 // Handle incoming comm msg.
75 // Handle incoming comm msg.
77 var method = msg.content.data.method;
76 var method = msg.content.data.method;
78 switch (method) {
77 switch (method) {
79 case 'update':
78 case 'update':
80 this.apply_update(msg.content.data.state);
79 this.apply_update(msg.content.data.state);
81 break;
80 break;
82 case 'custom':
81 case 'custom':
83 this.trigger('msg:custom', msg.content.data.content);
82 this.trigger('msg:custom', msg.content.data.content);
84 break;
83 break;
85 case 'display':
84 case 'display':
86 this.widget_manager.display_view(msg, this);
85 this.widget_manager.display_view(msg, this);
87 break;
86 break;
88 }
87 }
89 },
88 },
90
89
91 apply_update: function (state) {
90 apply_update: function (state) {
92 // Handle when a widget is updated via the python side.
91 // Handle when a widget is updated via the python side.
93 var that = this;
92 var that = this;
94 _.each(state, function(value, key) {
93 _.each(state, function(value, key) {
95 that.key_value_lock = [key, value];
94 that.key_value_lock = [key, value];
96 try {
95 try {
97 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
96 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
98 } finally {
97 } finally {
99 that.key_value_lock = null;
98 that.key_value_lock = null;
100 }
99 }
101 });
100 });
102 },
101 },
103
102
104 _handle_status: function (msg, callbacks) {
103 _handle_status: function (msg, callbacks) {
105 // Handle status msgs.
104 // Handle status msgs.
106
105
107 // execution_state : ('busy', 'idle', 'starting')
106 // execution_state : ('busy', 'idle', 'starting')
108 if (this.comm !== undefined) {
107 if (this.comm !== undefined) {
109 if (msg.content.execution_state ==='idle') {
108 if (msg.content.execution_state ==='idle') {
110 // Send buffer if this message caused another message to be
109 // Send buffer if this message caused another message to be
111 // throttled.
110 // throttled.
112 if (this.msg_buffer !== null &&
111 if (this.msg_buffer !== null &&
113 this.msg_throttle === this.pending_msgs) {
112 (this.get('msg_throttle') || 3) === this.pending_msgs) {
114 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
113 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
115 this.comm.send(data, callbacks);
114 this.comm.send(data, callbacks);
116 this.msg_buffer = null;
115 this.msg_buffer = null;
117 } else {
116 } else {
118 --this.pending_msgs;
117 --this.pending_msgs;
119 }
118 }
120 }
119 }
121 }
120 }
122 },
121 },
123
122
124 callbacks: function(view) {
123 callbacks: function(view) {
125 // Create msg callbacks for a comm msg.
124 // Create msg callbacks for a comm msg.
126 var callbacks = this.widget_manager.callbacks(view);
125 var callbacks = this.widget_manager.callbacks(view);
127
126
128 if (callbacks.iopub === undefined) {
127 if (callbacks.iopub === undefined) {
129 callbacks.iopub = {};
128 callbacks.iopub = {};
130 }
129 }
131
130
132 var that = this;
131 var that = this;
133 callbacks.iopub.status = function (msg) {
132 callbacks.iopub.status = function (msg) {
134 that._handle_status(msg, callbacks);
133 that._handle_status(msg, callbacks);
135 };
134 };
136 return callbacks;
135 return callbacks;
137 },
136 },
138
137
139 set: function(key, val, options) {
138 set: function(key, val, options) {
140 // Set a value.
139 // Set a value.
141 var return_value = WidgetModel.__super__.set.apply(this, arguments);
140 var return_value = WidgetModel.__super__.set.apply(this, arguments);
142
141
143 // Backbone only remembers the diff of the most recent set()
142 // Backbone only remembers the diff of the most recent set()
144 // operation. Calling set multiple times in a row results in a
143 // operation. Calling set multiple times in a row results in a
145 // loss of diff information. Here we keep our own running diff.
144 // loss of diff information. Here we keep our own running diff.
146 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
145 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
147 return return_value;
146 return return_value;
148 },
147 },
149
148
150 sync: function (method, model, options) {
149 sync: function (method, model, options) {
151 // Handle sync to the back-end. Called when a model.save() is called.
150 // Handle sync to the back-end. Called when a model.save() is called.
152
151
153 // Make sure a comm exists.
152 // Make sure a comm exists.
154 var error = options.error || function() {
153 var error = options.error || function() {
155 console.error('Backbone sync error:', arguments);
154 console.error('Backbone sync error:', arguments);
156 };
155 };
157 if (this.comm === undefined) {
156 if (this.comm === undefined) {
158 error();
157 error();
159 return false;
158 return false;
160 }
159 }
161
160
162 // Delete any key value pairs that the back-end already knows about.
161 // Delete any key value pairs that the back-end already knows about.
163 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
162 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
164 if (this.key_value_lock !== null) {
163 if (this.key_value_lock !== null) {
165 var key = this.key_value_lock[0];
164 var key = this.key_value_lock[0];
166 var value = this.key_value_lock[1];
165 var value = this.key_value_lock[1];
167 if (attrs[key] === value) {
166 if (attrs[key] === value) {
168 delete attrs[key];
167 delete attrs[key];
169 }
168 }
170 }
169 }
171
170
172 // Only sync if there are attributes to send to the back-end.
171 // Only sync if there are attributes to send to the back-end.
173 attrs = this._pack_models(attrs);
172 attrs = this._pack_models(attrs);
174 if (_.size(attrs) > 0) {
173 if (_.size(attrs) > 0) {
175
174
176 // If this message was sent via backbone itself, it will not
175 // If this message was sent via backbone itself, it will not
177 // have any callbacks. It's important that we create callbacks
176 // have any callbacks. It's important that we create callbacks
178 // so we can listen for status messages, etc...
177 // so we can listen for status messages, etc...
179 var callbacks = options.callbacks || this.callbacks();
178 var callbacks = options.callbacks || this.callbacks();
180
179
181 // Check throttle.
180 // Check throttle.
182 if (this.pending_msgs >= this.msg_throttle) {
181 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
183 // The throttle has been exceeded, buffer the current msg so
182 // The throttle has been exceeded, buffer the current msg so
184 // it can be sent once the kernel has finished processing
183 // it can be sent once the kernel has finished processing
185 // some of the existing messages.
184 // some of the existing messages.
186
185
187 // Combine updates if it is a 'patch' sync, otherwise replace updates
186 // Combine updates if it is a 'patch' sync, otherwise replace updates
188 switch (method) {
187 switch (method) {
189 case 'patch':
188 case 'patch':
190 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
189 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
191 break;
190 break;
192 case 'update':
191 case 'update':
193 case 'create':
192 case 'create':
194 this.msg_buffer = attrs;
193 this.msg_buffer = attrs;
195 break;
194 break;
196 default:
195 default:
197 error();
196 error();
198 return false;
197 return false;
199 }
198 }
200 this.msg_buffer_callbacks = callbacks;
199 this.msg_buffer_callbacks = callbacks;
201
200
202 } else {
201 } else {
203 // We haven't exceeded the throttle, send the message like
202 // We haven't exceeded the throttle, send the message like
204 // normal.
203 // normal.
205 var data = {method: 'backbone', sync_data: attrs};
204 var data = {method: 'backbone', sync_data: attrs};
206 this.comm.send(data, callbacks);
205 this.comm.send(data, callbacks);
207 this.pending_msgs++;
206 this.pending_msgs++;
208 }
207 }
209 }
208 }
210 // Since the comm is a one-way communication, assume the message
209 // Since the comm is a one-way communication, assume the message
211 // arrived. Don't call success since we don't have a model back from the server
210 // arrived. Don't call success since we don't have a model back from the server
212 // this means we miss out on the 'sync' event.
211 // this means we miss out on the 'sync' event.
213 this._buffered_state_diff = {};
212 this._buffered_state_diff = {};
214 },
213 },
215
214
216 save_changes: function(callbacks) {
215 save_changes: function(callbacks) {
217 // Push this model's state to the back-end
216 // Push this model's state to the back-end
218 //
217 //
219 // This invokes a Backbone.Sync.
218 // This invokes a Backbone.Sync.
220 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
219 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
221 },
220 },
222
221
223 _pack_models: function(value) {
222 _pack_models: function(value) {
224 // Replace models with model ids recursively.
223 // Replace models with model ids recursively.
225 if (value instanceof Backbone.Model) {
224 if (value instanceof Backbone.Model) {
226 return value.id;
225 return value.id;
227
226
228 } else if ($.isArray(value)) {
227 } else if ($.isArray(value)) {
229 var packed = [];
228 var packed = [];
230 var that = this;
229 var that = this;
231 _.each(value, function(sub_value, key) {
230 _.each(value, function(sub_value, key) {
232 packed.push(that._pack_models(sub_value));
231 packed.push(that._pack_models(sub_value));
233 });
232 });
234 return packed;
233 return packed;
235
234
236 } else if (value instanceof Object) {
235 } else if (value instanceof Object) {
237 var packed = {};
236 var packed = {};
238 var that = this;
237 var that = this;
239 _.each(value, function(sub_value, key) {
238 _.each(value, function(sub_value, key) {
240 packed[key] = that._pack_models(sub_value);
239 packed[key] = that._pack_models(sub_value);
241 });
240 });
242 return packed;
241 return packed;
243
242
244 } else {
243 } else {
245 return value;
244 return value;
246 }
245 }
247 },
246 },
248
247
249 _unpack_models: function(value) {
248 _unpack_models: function(value) {
250 // Replace model ids with models recursively.
249 // Replace model ids with models recursively.
251 if ($.isArray(value)) {
250 if ($.isArray(value)) {
252 var unpacked = [];
251 var unpacked = [];
253 var that = this;
252 var that = this;
254 _.each(value, function(sub_value, key) {
253 _.each(value, function(sub_value, key) {
255 unpacked.push(that._unpack_models(sub_value));
254 unpacked.push(that._unpack_models(sub_value));
256 });
255 });
257 return unpacked;
256 return unpacked;
258
257
259 } else if (value instanceof Object) {
258 } else if (value instanceof Object) {
260 var unpacked = {};
259 var unpacked = {};
261 var that = this;
260 var that = this;
262 _.each(value, function(sub_value, key) {
261 _.each(value, function(sub_value, key) {
263 unpacked[key] = that._unpack_models(sub_value);
262 unpacked[key] = that._unpack_models(sub_value);
264 });
263 });
265 return unpacked;
264 return unpacked;
266
265
267 } else {
266 } else {
268 var model = this.widget_manager.get_model(value);
267 var model = this.widget_manager.get_model(value);
269 if (model) {
268 if (model) {
270 return model;
269 return model;
271 } else {
270 } else {
272 return value;
271 return value;
273 }
272 }
274 }
273 }
275 },
274 },
276
275
277 });
276 });
278 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
277 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
279
278
280
279
281 var WidgetView = Backbone.View.extend({
280 var WidgetView = Backbone.View.extend({
282 initialize: function(parameters) {
281 initialize: function(parameters) {
283 // Public constructor.
282 // Public constructor.
284 this.model.on('change',this.update,this);
283 this.model.on('change',this.update,this);
285 this.options = parameters.options;
284 this.options = parameters.options;
286 this.child_views = [];
285 this.child_views = [];
287 this.model.views.push(this);
286 this.model.views.push(this);
288 },
287 },
289
288
290 update: function(){
289 update: function(){
291 // Triggered on model change.
290 // Triggered on model change.
292 //
291 //
293 // Update view to be consistent with this.model
292 // Update view to be consistent with this.model
294 },
293 },
295
294
296 create_child_view: function(child_model, options) {
295 create_child_view: function(child_model, options) {
297 // Create and return a child view.
296 // Create and return a child view.
298 //
297 //
299 // -given a model and (optionally) a view name if the view name is
298 // -given a model and (optionally) a view name if the view name is
300 // not given, it defaults to the model's default view attribute.
299 // not given, it defaults to the model's default view attribute.
301
300
302 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
301 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
303 // it would be great to have the widget manager add the cell metadata
302 // it would be great to have the widget manager add the cell metadata
304 // to the subview without having to add it here.
303 // to the subview without having to add it here.
305 var child_view = this.model.widget_manager.create_view(child_model, options || {}, this);
304 var child_view = this.model.widget_manager.create_view(child_model, options || {}, this);
306 this.child_views[child_model.id] = child_view;
305 this.child_views[child_model.id] = child_view;
307 return child_view;
306 return child_view;
308 },
307 },
309
308
310 delete_child_view: function(child_model, options) {
309 delete_child_view: function(child_model, options) {
311 // Delete a child view that was previously created using create_child_view.
310 // Delete a child view that was previously created using create_child_view.
312 var view = this.child_views[child_model.id];
311 var view = this.child_views[child_model.id];
313 if (view !== undefined) {
312 if (view !== undefined) {
314 delete this.child_views[child_model.id];
313 delete this.child_views[child_model.id];
315 view.remove();
314 view.remove();
316 }
315 }
317 },
316 },
318
317
319 do_diff: function(old_list, new_list, removed_callback, added_callback) {
318 do_diff: function(old_list, new_list, removed_callback, added_callback) {
320 // Difference a changed list and call remove and add callbacks for
319 // Difference a changed list and call remove and add callbacks for
321 // each removed and added item in the new list.
320 // each removed and added item in the new list.
322 //
321 //
323 // Parameters
322 // Parameters
324 // ----------
323 // ----------
325 // old_list : array
324 // old_list : array
326 // new_list : array
325 // new_list : array
327 // removed_callback : Callback(item)
326 // removed_callback : Callback(item)
328 // Callback that is called for each item removed.
327 // Callback that is called for each item removed.
329 // added_callback : Callback(item)
328 // added_callback : Callback(item)
330 // Callback that is called for each item added.
329 // Callback that is called for each item added.
331
330
332
331
333 // removed items
332 // removed items
334 _.each(_.difference(old_list, new_list), function(item, index, list) {
333 _.each(_.difference(old_list, new_list), function(item, index, list) {
335 removed_callback(item);
334 removed_callback(item);
336 }, this);
335 }, this);
337
336
338 // added items
337 // added items
339 _.each(_.difference(new_list, old_list), function(item, index, list) {
338 _.each(_.difference(new_list, old_list), function(item, index, list) {
340 added_callback(item);
339 added_callback(item);
341 }, this);
340 }, this);
342 },
341 },
343
342
344 callbacks: function(){
343 callbacks: function(){
345 // Create msg callbacks for a comm msg.
344 // Create msg callbacks for a comm msg.
346 return this.model.callbacks(this);
345 return this.model.callbacks(this);
347 },
346 },
348
347
349 render: function(){
348 render: function(){
350 // Render the view.
349 // Render the view.
351 //
350 //
352 // By default, this is only called the first time the view is created
351 // By default, this is only called the first time the view is created
353 },
352 },
354
353
355 send: function (content) {
354 send: function (content) {
356 // Send a custom msg associated with this view.
355 // Send a custom msg associated with this view.
357 this.model.send(content, this.callbacks());
356 this.model.send(content, this.callbacks());
358 },
357 },
359
358
360 touch: function () {
359 touch: function () {
361 this.model.save_changes(this.callbacks());
360 this.model.save_changes(this.callbacks());
362 },
361 },
363 });
362 });
364
363
365
364
366 var DOMWidgetView = WidgetView.extend({
365 var DOMWidgetView = WidgetView.extend({
367 initialize: function (options) {
366 initialize: function (options) {
368 // Public constructor
367 // Public constructor
369
368
370 // In the future we may want to make changes more granular
369 // In the future we may want to make changes more granular
371 // (e.g., trigger on visible:change).
370 // (e.g., trigger on visible:change).
372 this.model.on('change', this.update, this);
371 this.model.on('change', this.update, this);
373 this.model.on('msg:custom', this.on_msg, this);
372 this.model.on('msg:custom', this.on_msg, this);
374 DOMWidgetView.__super__.initialize.apply(this, arguments);
373 DOMWidgetView.__super__.initialize.apply(this, arguments);
375 },
374 },
376
375
377 on_msg: function(msg) {
376 on_msg: function(msg) {
378 // Handle DOM specific msgs.
377 // Handle DOM specific msgs.
379 switch(msg.msg_type) {
378 switch(msg.msg_type) {
380 case 'add_class':
379 case 'add_class':
381 this.add_class(msg.selector, msg.class_list);
380 this.add_class(msg.selector, msg.class_list);
382 break;
381 break;
383 case 'remove_class':
382 case 'remove_class':
384 this.remove_class(msg.selector, msg.class_list);
383 this.remove_class(msg.selector, msg.class_list);
385 break;
384 break;
386 }
385 }
387 },
386 },
388
387
389 add_class: function (selector, class_list) {
388 add_class: function (selector, class_list) {
390 // Add a DOM class to an element.
389 // Add a DOM class to an element.
391 this._get_selector_element(selector).addClass(class_list);
390 this._get_selector_element(selector).addClass(class_list);
392 },
391 },
393
392
394 remove_class: function (selector, class_list) {
393 remove_class: function (selector, class_list) {
395 // Remove a DOM class from an element.
394 // Remove a DOM class from an element.
396 this._get_selector_element(selector).removeClass(class_list);
395 this._get_selector_element(selector).removeClass(class_list);
397 },
396 },
398
397
399 update: function () {
398 update: function () {
400 // Update the contents of this view
399 // Update the contents of this view
401 //
400 //
402 // Called when the model is changed. The model may have been
401 // Called when the model is changed. The model may have been
403 // changed by another view or by a state update from the back-end.
402 // changed by another view or by a state update from the back-end.
404 // The very first update seems to happen before the element is
403 // The very first update seems to happen before the element is
405 // finished rendering so we use setTimeout to give the element time
404 // finished rendering so we use setTimeout to give the element time
406 // to render
405 // to render
407 var e = this.$el;
406 var e = this.$el;
408 var visible = this.model.get('visible');
407 var visible = this.model.get('visible');
409 setTimeout(function() {e.toggle(visible);},0);
408 setTimeout(function() {e.toggle(visible);},0);
410
409
411 var css = this.model.get('_css');
410 var css = this.model.get('_css');
412 if (css === undefined) {return;}
411 if (css === undefined) {return;}
413 var that = this;
412 var that = this;
414 _.each(css, function(css_traits, selector){
413 _.each(css, function(css_traits, selector){
415 // Apply the css traits to all elements that match the selector.
414 // Apply the css traits to all elements that match the selector.
416 var elements = that._get_selector_element(selector);
415 var elements = that._get_selector_element(selector);
417 if (elements.length > 0) {
416 if (elements.length > 0) {
418 _.each(css_traits, function(css_value, css_key){
417 _.each(css_traits, function(css_value, css_key){
419 elements.css(css_key, css_value);
418 elements.css(css_key, css_value);
420 });
419 });
421 }
420 }
422 });
421 });
423 },
422 },
424
423
425 _get_selector_element: function (selector) {
424 _get_selector_element: function (selector) {
426 // Get the elements via the css selector.
425 // Get the elements via the css selector.
427
426
428 // If the selector is blank, apply the style to the $el_to_style
427 // If the selector is blank, apply the style to the $el_to_style
429 // element. If the $el_to_style element is not defined, use apply
428 // element. If the $el_to_style element is not defined, use apply
430 // the style to the view's element.
429 // the style to the view's element.
431 var elements;
430 var elements;
432 if (!selector) {
431 if (!selector) {
433 if (this.$el_to_style === undefined) {
432 if (this.$el_to_style === undefined) {
434 elements = this.$el;
433 elements = this.$el;
435 } else {
434 } else {
436 elements = this.$el_to_style;
435 elements = this.$el_to_style;
437 }
436 }
438 } else {
437 } else {
439 elements = this.$el.find(selector);
438 elements = this.$el.find(selector);
440 }
439 }
441 return elements;
440 return elements;
442 },
441 },
443 });
442 });
444
443
445 IPython.WidgetModel = WidgetModel;
444 IPython.WidgetModel = WidgetModel;
446 IPython.WidgetView = WidgetView;
445 IPython.WidgetView = WidgetView;
447 IPython.DOMWidgetView = DOMWidgetView;
446 IPython.DOMWidgetView = DOMWidgetView;
448
447
449 // Pass through WidgetManager namespace.
448 // Pass through WidgetManager namespace.
450 return WidgetManager;
449 return WidgetManager;
451 });
450 });
@@ -1,441 +1,443
1 """Base Widget class. Allows user to create widgets in the back-end that render
1 """Base Widget class. Allows user to create widgets in the back-end that render
2 in the IPython notebook front-end.
2 in the IPython notebook front-end.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from contextlib import contextmanager
15 from contextlib import contextmanager
16
16
17 from IPython.core.getipython import get_ipython
17 from IPython.core.getipython import get_ipython
18 from IPython.kernel.comm import Comm
18 from IPython.kernel.comm import Comm
19 from IPython.config import LoggingConfigurable
19 from IPython.config import LoggingConfigurable
20 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple
20 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int
21 from IPython.utils.py3compat import string_types
21 from IPython.utils.py3compat import string_types
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Classes
24 # Classes
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 class CallbackDispatcher(LoggingConfigurable):
26 class CallbackDispatcher(LoggingConfigurable):
27 """A structure for registering and running callbacks"""
27 """A structure for registering and running callbacks"""
28 callbacks = List()
28 callbacks = List()
29
29
30 def __call__(self, *args, **kwargs):
30 def __call__(self, *args, **kwargs):
31 """Call all of the registered callbacks."""
31 """Call all of the registered callbacks."""
32 value = None
32 value = None
33 for callback in self.callbacks:
33 for callback in self.callbacks:
34 try:
34 try:
35 local_value = callback(*args, **kwargs)
35 local_value = callback(*args, **kwargs)
36 except Exception as e:
36 except Exception as e:
37 ip = get_ipython()
37 ip = get_ipython()
38 if ip is None:
38 if ip is None:
39 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
39 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
40 else:
40 else:
41 ip.showtraceback()
41 ip.showtraceback()
42 else:
42 else:
43 value = local_value if local_value is not None else value
43 value = local_value if local_value is not None else value
44 return value
44 return value
45
45
46 def register_callback(self, callback, remove=False):
46 def register_callback(self, callback, remove=False):
47 """(Un)Register a callback
47 """(Un)Register a callback
48
48
49 Parameters
49 Parameters
50 ----------
50 ----------
51 callback: method handle
51 callback: method handle
52 Method to be registered or unregistered.
52 Method to be registered or unregistered.
53 remove=False: bool
53 remove=False: bool
54 Whether to unregister the callback."""
54 Whether to unregister the callback."""
55
55
56 # (Un)Register the callback.
56 # (Un)Register the callback.
57 if remove and callback in self.callbacks:
57 if remove and callback in self.callbacks:
58 self.callbacks.remove(callback)
58 self.callbacks.remove(callback)
59 elif not remove and callback not in self.callbacks:
59 elif not remove and callback not in self.callbacks:
60 self.callbacks.append(callback)
60 self.callbacks.append(callback)
61
61
62 def _show_traceback(method):
62 def _show_traceback(method):
63 """decorator for showing tracebacks in IPython"""
63 """decorator for showing tracebacks in IPython"""
64 def m(self, *args, **kwargs):
64 def m(self, *args, **kwargs):
65 try:
65 try:
66 return(method(self, *args, **kwargs))
66 return(method(self, *args, **kwargs))
67 except Exception as e:
67 except Exception as e:
68 ip = get_ipython()
68 ip = get_ipython()
69 if ip is None:
69 if ip is None:
70 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
70 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
71 else:
71 else:
72 ip.showtraceback()
72 ip.showtraceback()
73 return m
73 return m
74
74
75 class Widget(LoggingConfigurable):
75 class Widget(LoggingConfigurable):
76 #-------------------------------------------------------------------------
76 #-------------------------------------------------------------------------
77 # Class attributes
77 # Class attributes
78 #-------------------------------------------------------------------------
78 #-------------------------------------------------------------------------
79 _widget_construction_callback = None
79 _widget_construction_callback = None
80 widgets = {}
80 widgets = {}
81
81
82 @staticmethod
82 @staticmethod
83 def on_widget_constructed(callback):
83 def on_widget_constructed(callback):
84 """Registers a callback to be called when a widget is constructed.
84 """Registers a callback to be called when a widget is constructed.
85
85
86 The callback must have the following signature:
86 The callback must have the following signature:
87 callback(widget)"""
87 callback(widget)"""
88 Widget._widget_construction_callback = callback
88 Widget._widget_construction_callback = callback
89
89
90 @staticmethod
90 @staticmethod
91 def _call_widget_constructed(widget):
91 def _call_widget_constructed(widget):
92 """Static method, called when a widget is constructed."""
92 """Static method, called when a widget is constructed."""
93 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
93 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
94 Widget._widget_construction_callback(widget)
94 Widget._widget_construction_callback(widget)
95
95
96 #-------------------------------------------------------------------------
96 #-------------------------------------------------------------------------
97 # Traits
97 # Traits
98 #-------------------------------------------------------------------------
98 #-------------------------------------------------------------------------
99 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
99 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
100 registered in the front-end to create and sync this widget with.""")
100 registered in the front-end to create and sync this widget with.""")
101 _view_name = Unicode(help="""Default view registered in the front-end
101 _view_name = Unicode(help="""Default view registered in the front-end
102 to use to represent the widget.""", sync=True)
102 to use to represent the widget.""", sync=True)
103 _comm = Instance('IPython.kernel.comm.Comm')
103 _comm = Instance('IPython.kernel.comm.Comm')
104
104
105 closed = Bool(False)
105 closed = Bool(False)
106 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
107 front-end can send before receiving an idle msg from the back-end.""")
106
108
107 keys = List()
109 keys = List()
108 def _keys_default(self):
110 def _keys_default(self):
109 return [name for name in self.traits(sync=True)]
111 return [name for name in self.traits(sync=True)]
110
112
111 _property_lock = Tuple((None, None))
113 _property_lock = Tuple((None, None))
112
114
113 _display_callbacks = Instance(CallbackDispatcher, ())
115 _display_callbacks = Instance(CallbackDispatcher, ())
114 _msg_callbacks = Instance(CallbackDispatcher, ())
116 _msg_callbacks = Instance(CallbackDispatcher, ())
115
117
116 #-------------------------------------------------------------------------
118 #-------------------------------------------------------------------------
117 # (Con/de)structor
119 # (Con/de)structor
118 #-------------------------------------------------------------------------
120 #-------------------------------------------------------------------------
119 def __init__(self, **kwargs):
121 def __init__(self, **kwargs):
120 """Public constructor"""
122 """Public constructor"""
121 super(Widget, self).__init__(**kwargs)
123 super(Widget, self).__init__(**kwargs)
122
124
123 self.on_trait_change(self._handle_property_changed, self.keys)
125 self.on_trait_change(self._handle_property_changed, self.keys)
124 Widget._call_widget_constructed(self)
126 Widget._call_widget_constructed(self)
125
127
126 def __del__(self):
128 def __del__(self):
127 """Object disposal"""
129 """Object disposal"""
128 self.close()
130 self.close()
129
131
130 #-------------------------------------------------------------------------
132 #-------------------------------------------------------------------------
131 # Properties
133 # Properties
132 #-------------------------------------------------------------------------
134 #-------------------------------------------------------------------------
133
135
134 @property
136 @property
135 def comm(self):
137 def comm(self):
136 """Gets the Comm associated with this widget.
138 """Gets the Comm associated with this widget.
137
139
138 If a Comm doesn't exist yet, a Comm will be created automagically."""
140 If a Comm doesn't exist yet, a Comm will be created automagically."""
139 if self._comm is None:
141 if self._comm is None:
140 # Create a comm.
142 # Create a comm.
141 self._comm = Comm(target_name=self._model_name)
143 self._comm = Comm(target_name=self._model_name)
142 self._comm.on_msg(self._handle_msg)
144 self._comm.on_msg(self._handle_msg)
143 self._comm.on_close(self._close)
145 self._comm.on_close(self._close)
144 Widget.widgets[self.model_id] = self
146 Widget.widgets[self.model_id] = self
145
147
146 # first update
148 # first update
147 self.send_state()
149 self.send_state()
148 return self._comm
150 return self._comm
149
151
150 @property
152 @property
151 def model_id(self):
153 def model_id(self):
152 """Gets the model id of this widget.
154 """Gets the model id of this widget.
153
155
154 If a Comm doesn't exist yet, a Comm will be created automagically."""
156 If a Comm doesn't exist yet, a Comm will be created automagically."""
155 return self.comm.comm_id
157 return self.comm.comm_id
156
158
157 #-------------------------------------------------------------------------
159 #-------------------------------------------------------------------------
158 # Methods
160 # Methods
159 #-------------------------------------------------------------------------
161 #-------------------------------------------------------------------------
160 def _close(self):
162 def _close(self):
161 """Private close - cleanup objects, registry entries"""
163 """Private close - cleanup objects, registry entries"""
162 del Widget.widgets[self.model_id]
164 del Widget.widgets[self.model_id]
163 self._comm = None
165 self._comm = None
164 self.closed = True
166 self.closed = True
165
167
166 def close(self):
168 def close(self):
167 """Close method.
169 """Close method.
168
170
169 Closes the widget which closes the underlying comm.
171 Closes the widget which closes the underlying comm.
170 When the comm is closed, all of the widget views are automatically
172 When the comm is closed, all of the widget views are automatically
171 removed from the front-end."""
173 removed from the front-end."""
172 if not self.closed:
174 if not self.closed:
173 self._comm.close()
175 self._comm.close()
174 self._close()
176 self._close()
175
177
176 def send_state(self, key=None):
178 def send_state(self, key=None):
177 """Sends the widget state, or a piece of it, to the front-end.
179 """Sends the widget state, or a piece of it, to the front-end.
178
180
179 Parameters
181 Parameters
180 ----------
182 ----------
181 key : unicode (optional)
183 key : unicode (optional)
182 A single property's name to sync with the front-end.
184 A single property's name to sync with the front-end.
183 """
185 """
184 self._send({
186 self._send({
185 "method" : "update",
187 "method" : "update",
186 "state" : self.get_state()
188 "state" : self.get_state()
187 })
189 })
188
190
189 def get_state(self, key=None):
191 def get_state(self, key=None):
190 """Gets the widget state, or a piece of it.
192 """Gets the widget state, or a piece of it.
191
193
192 Parameters
194 Parameters
193 ----------
195 ----------
194 key : unicode (optional)
196 key : unicode (optional)
195 A single property's name to get.
197 A single property's name to get.
196 """
198 """
197 keys = self.keys if key is None else [key]
199 keys = self.keys if key is None else [key]
198 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
200 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
199
201
200 def send(self, content):
202 def send(self, content):
201 """Sends a custom msg to the widget model in the front-end.
203 """Sends a custom msg to the widget model in the front-end.
202
204
203 Parameters
205 Parameters
204 ----------
206 ----------
205 content : dict
207 content : dict
206 Content of the message to send.
208 Content of the message to send.
207 """
209 """
208 self._send({"method": "custom", "content": content})
210 self._send({"method": "custom", "content": content})
209
211
210 def on_msg(self, callback, remove=False):
212 def on_msg(self, callback, remove=False):
211 """(Un)Register a custom msg receive callback.
213 """(Un)Register a custom msg receive callback.
212
214
213 Parameters
215 Parameters
214 ----------
216 ----------
215 callback: callable
217 callback: callable
216 callback will be passed two arguments when a message arrives::
218 callback will be passed two arguments when a message arrives::
217
219
218 callback(widget, content)
220 callback(widget, content)
219
221
220 remove: bool
222 remove: bool
221 True if the callback should be unregistered."""
223 True if the callback should be unregistered."""
222 self._msg_callbacks.register_callback(callback, remove=remove)
224 self._msg_callbacks.register_callback(callback, remove=remove)
223
225
224 def on_displayed(self, callback, remove=False):
226 def on_displayed(self, callback, remove=False):
225 """(Un)Register a widget displayed callback.
227 """(Un)Register a widget displayed callback.
226
228
227 Parameters
229 Parameters
228 ----------
230 ----------
229 callback: method handler
231 callback: method handler
230 Must have a signature of::
232 Must have a signature of::
231
233
232 callback(widget, **kwargs)
234 callback(widget, **kwargs)
233
235
234 kwargs from display are passed through without modification.
236 kwargs from display are passed through without modification.
235 remove: bool
237 remove: bool
236 True if the callback should be unregistered."""
238 True if the callback should be unregistered."""
237 self._display_callbacks.register_callback(callback, remove=remove)
239 self._display_callbacks.register_callback(callback, remove=remove)
238
240
239 #-------------------------------------------------------------------------
241 #-------------------------------------------------------------------------
240 # Support methods
242 # Support methods
241 #-------------------------------------------------------------------------
243 #-------------------------------------------------------------------------
242 @contextmanager
244 @contextmanager
243 def _lock_property(self, key, value):
245 def _lock_property(self, key, value):
244 """Lock a property-value pair.
246 """Lock a property-value pair.
245
247
246 NOTE: This, in addition to the single lock for all state changes, is
248 NOTE: This, in addition to the single lock for all state changes, is
247 flawed. In the future we may want to look into buffering state changes
249 flawed. In the future we may want to look into buffering state changes
248 back to the front-end."""
250 back to the front-end."""
249 self._property_lock = (key, value)
251 self._property_lock = (key, value)
250 try:
252 try:
251 yield
253 yield
252 finally:
254 finally:
253 self._property_lock = (None, None)
255 self._property_lock = (None, None)
254
256
255 def _should_send_property(self, key, value):
257 def _should_send_property(self, key, value):
256 """Check the property lock (property_lock)"""
258 """Check the property lock (property_lock)"""
257 return key != self._property_lock[0] or \
259 return key != self._property_lock[0] or \
258 value != self._property_lock[1]
260 value != self._property_lock[1]
259
261
260 # Event handlers
262 # Event handlers
261 @_show_traceback
263 @_show_traceback
262 def _handle_msg(self, msg):
264 def _handle_msg(self, msg):
263 """Called when a msg is received from the front-end"""
265 """Called when a msg is received from the front-end"""
264 data = msg['content']['data']
266 data = msg['content']['data']
265 method = data['method']
267 method = data['method']
266 if not method in ['backbone', 'custom']:
268 if not method in ['backbone', 'custom']:
267 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
269 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
268
270
269 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
271 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
270 if method == 'backbone' and 'sync_data' in data:
272 if method == 'backbone' and 'sync_data' in data:
271 sync_data = data['sync_data']
273 sync_data = data['sync_data']
272 self._handle_receive_state(sync_data) # handles all methods
274 self._handle_receive_state(sync_data) # handles all methods
273
275
274 # Handle a custom msg from the front-end
276 # Handle a custom msg from the front-end
275 elif method == 'custom':
277 elif method == 'custom':
276 if 'content' in data:
278 if 'content' in data:
277 self._handle_custom_msg(data['content'])
279 self._handle_custom_msg(data['content'])
278
280
279 def _handle_receive_state(self, sync_data):
281 def _handle_receive_state(self, sync_data):
280 """Called when a state is received from the front-end."""
282 """Called when a state is received from the front-end."""
281 for name in self.keys:
283 for name in self.keys:
282 if name in sync_data:
284 if name in sync_data:
283 value = self._unpack_widgets(sync_data[name])
285 value = self._unpack_widgets(sync_data[name])
284 with self._lock_property(name, value):
286 with self._lock_property(name, value):
285 setattr(self, name, value)
287 setattr(self, name, value)
286
288
287 def _handle_custom_msg(self, content):
289 def _handle_custom_msg(self, content):
288 """Called when a custom msg is received."""
290 """Called when a custom msg is received."""
289 self._msg_callbacks(self, content)
291 self._msg_callbacks(self, content)
290
292
291 def _handle_property_changed(self, name, old, new):
293 def _handle_property_changed(self, name, old, new):
292 """Called when a property has been changed."""
294 """Called when a property has been changed."""
293 # Make sure this isn't information that the front-end just sent us.
295 # Make sure this isn't information that the front-end just sent us.
294 if self._should_send_property(name, new):
296 if self._should_send_property(name, new):
295 # Send new state to front-end
297 # Send new state to front-end
296 self.send_state(key=name)
298 self.send_state(key=name)
297
299
298 def _handle_displayed(self, **kwargs):
300 def _handle_displayed(self, **kwargs):
299 """Called when a view has been displayed for this widget instance"""
301 """Called when a view has been displayed for this widget instance"""
300 self._display_callbacks(self, **kwargs)
302 self._display_callbacks(self, **kwargs)
301
303
302 def _pack_widgets(self, x):
304 def _pack_widgets(self, x):
303 """Recursively converts all widget instances to model id strings.
305 """Recursively converts all widget instances to model id strings.
304
306
305 Children widgets will be stored and transmitted to the front-end by
307 Children widgets will be stored and transmitted to the front-end by
306 their model ids. Return value must be JSON-able."""
308 their model ids. Return value must be JSON-able."""
307 if isinstance(x, dict):
309 if isinstance(x, dict):
308 return {k: self._pack_widgets(v) for k, v in x.items()}
310 return {k: self._pack_widgets(v) for k, v in x.items()}
309 elif isinstance(x, list):
311 elif isinstance(x, list):
310 return [self._pack_widgets(v) for v in x]
312 return [self._pack_widgets(v) for v in x]
311 elif isinstance(x, Widget):
313 elif isinstance(x, Widget):
312 return x.model_id
314 return x.model_id
313 else:
315 else:
314 return x # Value must be JSON-able
316 return x # Value must be JSON-able
315
317
316 def _unpack_widgets(self, x):
318 def _unpack_widgets(self, x):
317 """Recursively converts all model id strings to widget instances.
319 """Recursively converts all model id strings to widget instances.
318
320
319 Children widgets will be stored and transmitted to the front-end by
321 Children widgets will be stored and transmitted to the front-end by
320 their model ids."""
322 their model ids."""
321 if isinstance(x, dict):
323 if isinstance(x, dict):
322 return {k: self._unpack_widgets(v) for k, v in x.items()}
324 return {k: self._unpack_widgets(v) for k, v in x.items()}
323 elif isinstance(x, list):
325 elif isinstance(x, list):
324 return [self._unpack_widgets(v) for v in x]
326 return [self._unpack_widgets(v) for v in x]
325 elif isinstance(x, string_types):
327 elif isinstance(x, string_types):
326 return x if x not in Widget.widgets else Widget.widgets[x]
328 return x if x not in Widget.widgets else Widget.widgets[x]
327 else:
329 else:
328 return x
330 return x
329
331
330 def _ipython_display_(self, **kwargs):
332 def _ipython_display_(self, **kwargs):
331 """Called when `IPython.display.display` is called on the widget."""
333 """Called when `IPython.display.display` is called on the widget."""
332 # Show view. By sending a display message, the comm is opened and the
334 # Show view. By sending a display message, the comm is opened and the
333 # initial state is sent.
335 # initial state is sent.
334 self._send({"method": "display"})
336 self._send({"method": "display"})
335 self._handle_displayed(**kwargs)
337 self._handle_displayed(**kwargs)
336
338
337 def _send(self, msg):
339 def _send(self, msg):
338 """Sends a message to the model in the front-end."""
340 """Sends a message to the model in the front-end."""
339 self.comm.send(msg)
341 self.comm.send(msg)
340
342
341
343
342 class DOMWidget(Widget):
344 class DOMWidget(Widget):
343 visible = Bool(True, help="Whether the widget is visible.", sync=True)
345 visible = Bool(True, help="Whether the widget is visible.", sync=True)
344 _css = Dict(sync=True) # Internal CSS property dict
346 _css = Dict(sync=True) # Internal CSS property dict
345
347
346 def get_css(self, key, selector=""):
348 def get_css(self, key, selector=""):
347 """Get a CSS property of the widget.
349 """Get a CSS property of the widget.
348
350
349 Note: This function does not actually request the CSS from the
351 Note: This function does not actually request the CSS from the
350 front-end; Only properties that have been set with set_css can be read.
352 front-end; Only properties that have been set with set_css can be read.
351
353
352 Parameters
354 Parameters
353 ----------
355 ----------
354 key: unicode
356 key: unicode
355 CSS key
357 CSS key
356 selector: unicode (optional)
358 selector: unicode (optional)
357 JQuery selector used when the CSS key/value was set.
359 JQuery selector used when the CSS key/value was set.
358 """
360 """
359 if selector in self._css and key in self._css[selector]:
361 if selector in self._css and key in self._css[selector]:
360 return self._css[selector][key]
362 return self._css[selector][key]
361 else:
363 else:
362 return None
364 return None
363
365
364 def set_css(self, dict_or_key, value=None, selector=''):
366 def set_css(self, dict_or_key, value=None, selector=''):
365 """Set one or more CSS properties of the widget.
367 """Set one or more CSS properties of the widget.
366
368
367 This function has two signatures:
369 This function has two signatures:
368 - set_css(css_dict, selector='')
370 - set_css(css_dict, selector='')
369 - set_css(key, value, selector='')
371 - set_css(key, value, selector='')
370
372
371 Parameters
373 Parameters
372 ----------
374 ----------
373 css_dict : dict
375 css_dict : dict
374 CSS key/value pairs to apply
376 CSS key/value pairs to apply
375 key: unicode
377 key: unicode
376 CSS key
378 CSS key
377 value:
379 value:
378 CSS value
380 CSS value
379 selector: unicode (optional, kwarg only)
381 selector: unicode (optional, kwarg only)
380 JQuery selector to use to apply the CSS key/value. If no selector
382 JQuery selector to use to apply the CSS key/value. If no selector
381 is provided, an empty selector is used. An empty selector makes the
383 is provided, an empty selector is used. An empty selector makes the
382 front-end try to apply the css to a default element. The default
384 front-end try to apply the css to a default element. The default
383 element is an attribute unique to each view, which is a DOM element
385 element is an attribute unique to each view, which is a DOM element
384 of the view that should be styled with common CSS (see
386 of the view that should be styled with common CSS (see
385 `$el_to_style` in the Javascript code).
387 `$el_to_style` in the Javascript code).
386 """
388 """
387 if not selector in self._css:
389 if not selector in self._css:
388 self._css[selector] = {}
390 self._css[selector] = {}
389 my_css = self._css[selector]
391 my_css = self._css[selector]
390
392
391 if value is None:
393 if value is None:
392 css_dict = dict_or_key
394 css_dict = dict_or_key
393 else:
395 else:
394 css_dict = {dict_or_key: value}
396 css_dict = {dict_or_key: value}
395
397
396 for (key, value) in css_dict.items():
398 for (key, value) in css_dict.items():
397 if not (key in my_css and value == my_css[key]):
399 if not (key in my_css and value == my_css[key]):
398 my_css[key] = value
400 my_css[key] = value
399 self.send_state('_css')
401 self.send_state('_css')
400
402
401 def add_class(self, class_names, selector=""):
403 def add_class(self, class_names, selector=""):
402 """Add class[es] to a DOM element.
404 """Add class[es] to a DOM element.
403
405
404 Parameters
406 Parameters
405 ----------
407 ----------
406 class_names: unicode or list
408 class_names: unicode or list
407 Class name(s) to add to the DOM element(s).
409 Class name(s) to add to the DOM element(s).
408 selector: unicode (optional)
410 selector: unicode (optional)
409 JQuery selector to select the DOM element(s) that the class(es) will
411 JQuery selector to select the DOM element(s) that the class(es) will
410 be added to.
412 be added to.
411 """
413 """
412 class_list = class_names
414 class_list = class_names
413 if isinstance(class_list, list):
415 if isinstance(class_list, list):
414 class_list = ' '.join(class_list)
416 class_list = ' '.join(class_list)
415
417
416 self.send({
418 self.send({
417 "msg_type" : "add_class",
419 "msg_type" : "add_class",
418 "class_list" : class_list,
420 "class_list" : class_list,
419 "selector" : selector
421 "selector" : selector
420 })
422 })
421
423
422 def remove_class(self, class_names, selector=""):
424 def remove_class(self, class_names, selector=""):
423 """Remove class[es] from a DOM element.
425 """Remove class[es] from a DOM element.
424
426
425 Parameters
427 Parameters
426 ----------
428 ----------
427 class_names: unicode or list
429 class_names: unicode or list
428 Class name(s) to remove from the DOM element(s).
430 Class name(s) to remove from the DOM element(s).
429 selector: unicode (optional)
431 selector: unicode (optional)
430 JQuery selector to select the DOM element(s) that the class(es) will
432 JQuery selector to select the DOM element(s) that the class(es) will
431 be removed from.
433 be removed from.
432 """
434 """
433 class_list = class_names
435 class_list = class_names
434 if isinstance(class_list, list):
436 if isinstance(class_list, list):
435 class_list = ' '.join(class_list)
437 class_list = ' '.join(class_list)
436
438
437 self.send({
439 self.send({
438 "msg_type" : "remove_class",
440 "msg_type" : "remove_class",
439 "class_list" : class_list,
441 "class_list" : class_list,
440 "selector" : selector,
442 "selector" : selector,
441 })
443 })
General Comments 0
You need to be logged in to leave comments. Login now