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