##// END OF EJS Templates
Change serialize/deserialize to to_json/from_json
Jason Grout -
Show More
@@ -1,453 +1,453 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 state = {}
198 state = {}
199 for k in keys:
199 for k in keys:
200 f = self.trait_metadata(k, 'serialize')
200 f = self.trait_metadata(k, 'to_json')
201 if f is None:
201 if f is None:
202 f = self._serialize_trait
202 f = self._trait_to_json
203 value = getattr(self, k)
203 value = getattr(self, k)
204 state[k] = f(value)
204 state[k] = f(value)
205 return state
205 return state
206
206
207 def send(self, content):
207 def send(self, content):
208 """Sends a custom msg to the widget model in the front-end.
208 """Sends a custom msg to the widget model in the front-end.
209
209
210 Parameters
210 Parameters
211 ----------
211 ----------
212 content : dict
212 content : dict
213 Content of the message to send.
213 Content of the message to send.
214 """
214 """
215 self._send({"method": "custom", "content": content})
215 self._send({"method": "custom", "content": content})
216
216
217 def on_msg(self, callback, remove=False):
217 def on_msg(self, callback, remove=False):
218 """(Un)Register a custom msg receive callback.
218 """(Un)Register a custom msg receive callback.
219
219
220 Parameters
220 Parameters
221 ----------
221 ----------
222 callback: callable
222 callback: callable
223 callback will be passed two arguments when a message arrives::
223 callback will be passed two arguments when a message arrives::
224
224
225 callback(widget, content)
225 callback(widget, content)
226
226
227 remove: bool
227 remove: bool
228 True if the callback should be unregistered."""
228 True if the callback should be unregistered."""
229 self._msg_callbacks.register_callback(callback, remove=remove)
229 self._msg_callbacks.register_callback(callback, remove=remove)
230
230
231 def on_displayed(self, callback, remove=False):
231 def on_displayed(self, callback, remove=False):
232 """(Un)Register a widget displayed callback.
232 """(Un)Register a widget displayed callback.
233
233
234 Parameters
234 Parameters
235 ----------
235 ----------
236 callback: method handler
236 callback: method handler
237 Must have a signature of::
237 Must have a signature of::
238
238
239 callback(widget, **kwargs)
239 callback(widget, **kwargs)
240
240
241 kwargs from display are passed through without modification.
241 kwargs from display are passed through without modification.
242 remove: bool
242 remove: bool
243 True if the callback should be unregistered."""
243 True if the callback should be unregistered."""
244 self._display_callbacks.register_callback(callback, remove=remove)
244 self._display_callbacks.register_callback(callback, remove=remove)
245
245
246 #-------------------------------------------------------------------------
246 #-------------------------------------------------------------------------
247 # Support methods
247 # Support methods
248 #-------------------------------------------------------------------------
248 #-------------------------------------------------------------------------
249 @contextmanager
249 @contextmanager
250 def _lock_property(self, key, value):
250 def _lock_property(self, key, value):
251 """Lock a property-value pair.
251 """Lock a property-value pair.
252
252
253 NOTE: This, in addition to the single lock for all state changes, is
253 NOTE: This, in addition to the single lock for all state changes, is
254 flawed. In the future we may want to look into buffering state changes
254 flawed. In the future we may want to look into buffering state changes
255 back to the front-end."""
255 back to the front-end."""
256 self._property_lock = (key, value)
256 self._property_lock = (key, value)
257 try:
257 try:
258 yield
258 yield
259 finally:
259 finally:
260 self._property_lock = (None, None)
260 self._property_lock = (None, None)
261
261
262 def _should_send_property(self, key, value):
262 def _should_send_property(self, key, value):
263 """Check the property lock (property_lock)"""
263 """Check the property lock (property_lock)"""
264 return key != self._property_lock[0] or \
264 return key != self._property_lock[0] or \
265 value != self._property_lock[1]
265 value != self._property_lock[1]
266
266
267 # Event handlers
267 # Event handlers
268 @_show_traceback
268 @_show_traceback
269 def _handle_msg(self, msg):
269 def _handle_msg(self, msg):
270 """Called when a msg is received from the front-end"""
270 """Called when a msg is received from the front-end"""
271 data = msg['content']['data']
271 data = msg['content']['data']
272 method = data['method']
272 method = data['method']
273 if not method in ['backbone', 'custom']:
273 if not method in ['backbone', 'custom']:
274 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
274 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
275
275
276 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
276 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
277 if method == 'backbone' and 'sync_data' in data:
277 if method == 'backbone' and 'sync_data' in data:
278 sync_data = data['sync_data']
278 sync_data = data['sync_data']
279 self._handle_receive_state(sync_data) # handles all methods
279 self._handle_receive_state(sync_data) # handles all methods
280
280
281 # Handle a custom msg from the front-end
281 # Handle a custom msg from the front-end
282 elif method == 'custom':
282 elif method == 'custom':
283 if 'content' in data:
283 if 'content' in data:
284 self._handle_custom_msg(data['content'])
284 self._handle_custom_msg(data['content'])
285
285
286 def _handle_receive_state(self, sync_data):
286 def _handle_receive_state(self, sync_data):
287 """Called when a state is received from the front-end."""
287 """Called when a state is received from the front-end."""
288 for name in self.keys:
288 for name in self.keys:
289 if name in sync_data:
289 if name in sync_data:
290 f = self.trait_metadata(name, 'deserialize')
290 f = self.trait_metadata(name, 'from_json')
291 if f is None:
291 if f is None:
292 f = self._deserialize_trait
292 f = self._trait_from_json
293 value = f(sync_data[name])
293 value = f(sync_data[name])
294 with self._lock_property(name, value):
294 with self._lock_property(name, value):
295 setattr(self, name, value)
295 setattr(self, name, value)
296
296
297 def _handle_custom_msg(self, content):
297 def _handle_custom_msg(self, content):
298 """Called when a custom msg is received."""
298 """Called when a custom msg is received."""
299 self._msg_callbacks(self, content)
299 self._msg_callbacks(self, content)
300
300
301 def _handle_property_changed(self, name, old, new):
301 def _handle_property_changed(self, name, old, new):
302 """Called when a property has been changed."""
302 """Called when a property has been changed."""
303 # Make sure this isn't information that the front-end just sent us.
303 # Make sure this isn't information that the front-end just sent us.
304 if self._should_send_property(name, new):
304 if self._should_send_property(name, new):
305 # Send new state to front-end
305 # Send new state to front-end
306 self.send_state(key=name)
306 self.send_state(key=name)
307
307
308 def _handle_displayed(self, **kwargs):
308 def _handle_displayed(self, **kwargs):
309 """Called when a view has been displayed for this widget instance"""
309 """Called when a view has been displayed for this widget instance"""
310 self._display_callbacks(self, **kwargs)
310 self._display_callbacks(self, **kwargs)
311
311
312 def _serialize_trait(self, x):
312 def _trait_to_json(self, x):
313 """Serialize a trait value to json
313 """Convert a trait value to json
314
314
315 Traverse lists/tuples and dicts and serialize their values as well.
315 Traverse lists/tuples and dicts and serialize their values as well.
316 Replace any widgets with their model_id
316 Replace any widgets with their model_id
317 """
317 """
318 if isinstance(x, dict):
318 if isinstance(x, dict):
319 return {k: self._serialize_trait(v) for k, v in x.items()}
319 return {k: self._trait_to_json(v) for k, v in x.items()}
320 elif isinstance(x, (list, tuple)):
320 elif isinstance(x, (list, tuple)):
321 return [self._serialize_trait(v) for v in x]
321 return [self._trait_to_json(v) for v in x]
322 elif isinstance(x, Widget):
322 elif isinstance(x, Widget):
323 return "IPY_MODEL_" + x.model_id
323 return "IPY_MODEL_" + x.model_id
324 else:
324 else:
325 return x # Value must be JSON-able
325 return x # Value must be JSON-able
326
326
327 def _deserialize_trait(self, x):
327 def _trait_from_json(self, x):
328 """Convert json values to objects
328 """Convert json values to objects
329
329
330 We explicitly support converting valid string widget UUIDs to Widget references.
330 Replace any strings representing valid model id values to Widget references.
331 """
331 """
332 if isinstance(x, dict):
332 if isinstance(x, dict):
333 return {k: self._deserialize_trait(v) for k, v in x.items()}
333 return {k: self._trait_from_json(v) for k, v in x.items()}
334 elif isinstance(x, (list, tuple)):
334 elif isinstance(x, (list, tuple)):
335 return [self._deserialize_trait(v) for v in x]
335 return [self._trait_from_json(v) for v in x]
336 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
336 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
337 # we want to support having child widgets at any level in a hierarchy
337 # we want to support having child widgets at any level in a hierarchy
338 # trusting that a widget UUID will not appear out in the wild
338 # trusting that a widget UUID will not appear out in the wild
339 return Widget.widgets[x]
339 return Widget.widgets[x]
340 else:
340 else:
341 return x
341 return x
342
342
343 def _ipython_display_(self, **kwargs):
343 def _ipython_display_(self, **kwargs):
344 """Called when `IPython.display.display` is called on the widget."""
344 """Called when `IPython.display.display` is called on the widget."""
345 # Show view. By sending a display message, the comm is opened and the
345 # Show view. By sending a display message, the comm is opened and the
346 # initial state is sent.
346 # initial state is sent.
347 self._send({"method": "display"})
347 self._send({"method": "display"})
348 self._handle_displayed(**kwargs)
348 self._handle_displayed(**kwargs)
349
349
350 def _send(self, msg):
350 def _send(self, msg):
351 """Sends a message to the model in the front-end."""
351 """Sends a message to the model in the front-end."""
352 self.comm.send(msg)
352 self.comm.send(msg)
353
353
354
354
355 class DOMWidget(Widget):
355 class DOMWidget(Widget):
356 visible = Bool(True, help="Whether the widget is visible.", sync=True)
356 visible = Bool(True, help="Whether the widget is visible.", sync=True)
357 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
357 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
358
358
359 def get_css(self, key, selector=""):
359 def get_css(self, key, selector=""):
360 """Get a CSS property of the widget.
360 """Get a CSS property of the widget.
361
361
362 Note: This function does not actually request the CSS from the
362 Note: This function does not actually request the CSS from the
363 front-end; Only properties that have been set with set_css can be read.
363 front-end; Only properties that have been set with set_css can be read.
364
364
365 Parameters
365 Parameters
366 ----------
366 ----------
367 key: unicode
367 key: unicode
368 CSS key
368 CSS key
369 selector: unicode (optional)
369 selector: unicode (optional)
370 JQuery selector used when the CSS key/value was set.
370 JQuery selector used when the CSS key/value was set.
371 """
371 """
372 if selector in self._css and key in self._css[selector]:
372 if selector in self._css and key in self._css[selector]:
373 return self._css[selector][key]
373 return self._css[selector][key]
374 else:
374 else:
375 return None
375 return None
376
376
377 def set_css(self, dict_or_key, value=None, selector=''):
377 def set_css(self, dict_or_key, value=None, selector=''):
378 """Set one or more CSS properties of the widget.
378 """Set one or more CSS properties of the widget.
379
379
380 This function has two signatures:
380 This function has two signatures:
381 - set_css(css_dict, selector='')
381 - set_css(css_dict, selector='')
382 - set_css(key, value, selector='')
382 - set_css(key, value, selector='')
383
383
384 Parameters
384 Parameters
385 ----------
385 ----------
386 css_dict : dict
386 css_dict : dict
387 CSS key/value pairs to apply
387 CSS key/value pairs to apply
388 key: unicode
388 key: unicode
389 CSS key
389 CSS key
390 value:
390 value:
391 CSS value
391 CSS value
392 selector: unicode (optional, kwarg only)
392 selector: unicode (optional, kwarg only)
393 JQuery selector to use to apply the CSS key/value. If no selector
393 JQuery selector to use to apply the CSS key/value. If no selector
394 is provided, an empty selector is used. An empty selector makes the
394 is provided, an empty selector is used. An empty selector makes the
395 front-end try to apply the css to a default element. The default
395 front-end try to apply the css to a default element. The default
396 element is an attribute unique to each view, which is a DOM element
396 element is an attribute unique to each view, which is a DOM element
397 of the view that should be styled with common CSS (see
397 of the view that should be styled with common CSS (see
398 `$el_to_style` in the Javascript code).
398 `$el_to_style` in the Javascript code).
399 """
399 """
400 if value is None:
400 if value is None:
401 css_dict = dict_or_key
401 css_dict = dict_or_key
402 else:
402 else:
403 css_dict = {dict_or_key: value}
403 css_dict = {dict_or_key: value}
404
404
405 for (key, value) in css_dict.items():
405 for (key, value) in css_dict.items():
406 # First remove the selector/key pair from the css list if it exists.
406 # First remove the selector/key pair from the css list if it exists.
407 # Then add the selector/key pair and new value to the bottom of the
407 # Then add the selector/key pair and new value to the bottom of the
408 # list.
408 # list.
409 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
409 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
410 self._css += [(selector, key, value)]
410 self._css += [(selector, key, value)]
411 self.send_state('_css')
411 self.send_state('_css')
412
412
413 def add_class(self, class_names, selector=""):
413 def add_class(self, class_names, selector=""):
414 """Add class[es] to a DOM element.
414 """Add class[es] to a DOM element.
415
415
416 Parameters
416 Parameters
417 ----------
417 ----------
418 class_names: unicode or list
418 class_names: unicode or list
419 Class name(s) to add to the DOM element(s).
419 Class name(s) to add to the DOM element(s).
420 selector: unicode (optional)
420 selector: unicode (optional)
421 JQuery selector to select the DOM element(s) that the class(es) will
421 JQuery selector to select the DOM element(s) that the class(es) will
422 be added to.
422 be added to.
423 """
423 """
424 class_list = class_names
424 class_list = class_names
425 if isinstance(class_list, (list, tuple)):
425 if isinstance(class_list, (list, tuple)):
426 class_list = ' '.join(class_list)
426 class_list = ' '.join(class_list)
427
427
428 self.send({
428 self.send({
429 "msg_type" : "add_class",
429 "msg_type" : "add_class",
430 "class_list" : class_list,
430 "class_list" : class_list,
431 "selector" : selector
431 "selector" : selector
432 })
432 })
433
433
434 def remove_class(self, class_names, selector=""):
434 def remove_class(self, class_names, selector=""):
435 """Remove class[es] from a DOM element.
435 """Remove class[es] from a DOM element.
436
436
437 Parameters
437 Parameters
438 ----------
438 ----------
439 class_names: unicode or list
439 class_names: unicode or list
440 Class name(s) to remove from the DOM element(s).
440 Class name(s) to remove from the DOM element(s).
441 selector: unicode (optional)
441 selector: unicode (optional)
442 JQuery selector to select the DOM element(s) that the class(es) will
442 JQuery selector to select the DOM element(s) that the class(es) will
443 be removed from.
443 be removed from.
444 """
444 """
445 class_list = class_names
445 class_list = class_names
446 if isinstance(class_list, (list, tuple)):
446 if isinstance(class_list, (list, tuple)):
447 class_list = ' '.join(class_list)
447 class_list = ' '.join(class_list)
448
448
449 self.send({
449 self.send({
450 "msg_type" : "remove_class",
450 "msg_type" : "remove_class",
451 "class_list" : class_list,
451 "class_list" : class_list,
452 "selector" : selector,
452 "selector" : selector,
453 })
453 })
General Comments 0
You need to be logged in to leave comments. Login now