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