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