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