##// END OF EJS Templates
Merge pull request #6494 from takluyver/widget-comm-require...
Jonathan Frederic -
r18443:d6414f0f merge
parent child Browse files
Show More
@@ -1,190 +1,204 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/namespace"
9 9 ], function (_, Backbone, $, IPython) {
10
10 "use strict";
11 11 //--------------------------------------------------------------------
12 12 // WidgetManager class
13 13 //--------------------------------------------------------------------
14 14 var WidgetManager = function (comm_manager, notebook) {
15 15 // Public constructor
16 16 WidgetManager._managers.push(this);
17 17
18 18 // Attach a comm manager to the
19 19 this.keyboard_manager = notebook.keyboard_manager;
20 20 this.notebook = notebook;
21 21 this.comm_manager = comm_manager;
22 22 this._models = {}; /* Dictionary of model ids and model instances */
23 23
24 24 // Register with the comm manager.
25 25 this.comm_manager.register_target('ipython.widget', $.proxy(this._handle_comm_open, this));
26 26 };
27 27
28 28 //--------------------------------------------------------------------
29 29 // Class level
30 30 //--------------------------------------------------------------------
31 31 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
32 32 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
33 33 WidgetManager._managers = []; /* List of widget managers */
34 34
35 35 WidgetManager.register_widget_model = function (model_name, model_type) {
36 36 // Registers a widget model by name.
37 37 WidgetManager._model_types[model_name] = model_type;
38 38 };
39 39
40 40 WidgetManager.register_widget_view = function (view_name, view_type) {
41 41 // Registers a widget view by name.
42 42 WidgetManager._view_types[view_name] = view_type;
43 43 };
44 44
45 45 //--------------------------------------------------------------------
46 46 // Instance level
47 47 //--------------------------------------------------------------------
48 48 WidgetManager.prototype.display_view = function(msg, model) {
49 49 // Displays a view for a particular model.
50 50 var cell = this.get_msg_cell(msg.parent_header.msg_id);
51 51 if (cell === null) {
52 52 console.log("Could not determine where the display" +
53 53 " message was from. Widget will not be displayed");
54 54 } else {
55 var view = this.create_view(model, {cell: cell});
56 if (view === null) {
57 console.error("View creation failed", model);
58 }
59 this._handle_display_view(view);
60 if (cell.widget_subarea) {
61 cell.widget_subarea.append(view.$el);
62 }
63 view.trigger('displayed');
55 var that = this;
56 this.create_view(model, {cell: cell, callback: function(view) {
57 that._handle_display_view(view);
58 if (cell.widget_subarea) {
59 cell.widget_subarea.append(view.$el);
60 }
61 view.trigger('displayed');
62 }});
64 63 }
65 64 };
66 65
67 66 WidgetManager.prototype._handle_display_view = function (view) {
68 67 // Have the IPython keyboard manager disable its event
69 68 // handling so the widget can capture keyboard input.
70 69 // Note, this is only done on the outer most widgets.
71 70 if (this.keyboard_manager) {
72 71 this.keyboard_manager.register_events(view.$el);
73 72
74 73 if (view.additional_elements) {
75 74 for (var i = 0; i < view.additional_elements.length; i++) {
76 75 this.keyboard_manager.register_events(view.additional_elements[i]);
77 76 }
78 77 }
79 78 }
80 79 };
80
81 81
82 WidgetManager.prototype.create_view = function(model, options, view) {
82 WidgetManager.prototype.create_view = function(model, options) {
83 83 // Creates a view for a particular model.
84
84 85 var view_name = model.get('_view_name');
85 var ViewType = WidgetManager._view_types[view_name];
86 if (ViewType) {
87
88 // If a view is passed into the method, use that view's cell as
89 // the cell for the view that is created.
90 options = options || {};
91 if (view !== undefined) {
92 options.cell = view.options.cell;
86 var view_mod = model.get('_view_module');
87 var errback = options.errback || function(err) {console.log(err);};
88
89 var instantiate_view = function(ViewType) {
90 if (ViewType) {
91 // If a view is passed into the method, use that view's cell as
92 // the cell for the view that is created.
93 options = options || {};
94 if (options.parent !== undefined) {
95 options.cell = options.parent.options.cell;
96 }
97
98 // Create and render the view...
99 var parameters = {model: model, options: options};
100 var view = new ViewType(parameters);
101 view.render();
102 model.on('destroy', view.remove, view);
103 options.callback(view);
104 } else {
105 errback({unknown_view: true, view_name: view_name,
106 view_module: view_mod});
93 107 }
108 };
94 109
95 // Create and render the view...
96 var parameters = {model: model, options: options};
97 view = new ViewType(parameters);
98 view.render();
99 model.on('destroy', view.remove, view);
100 return view;
110 if (view_mod) {
111 require([view_mod], function(module) {
112 instantiate_view(module[view_name]);
113 }, errback);
114 } else {
115 instantiate_view(WidgetManager._view_types[view_name]);
101 116 }
102 return null;
103 117 };
104 118
105 119 WidgetManager.prototype.get_msg_cell = function (msg_id) {
106 120 var cell = null;
107 121 // First, check to see if the msg was triggered by cell execution.
108 122 if (this.notebook) {
109 123 cell = this.notebook.get_msg_cell(msg_id);
110 124 }
111 125 if (cell !== null) {
112 126 return cell;
113 127 }
114 128 // Second, check to see if a get_cell callback was defined
115 129 // for the message. get_cell callbacks are registered for
116 130 // widget messages, so this block is actually checking to see if the
117 131 // message was triggered by a widget.
118 132 var kernel = this.comm_manager.kernel;
119 133 if (kernel) {
120 134 var callbacks = kernel.get_callbacks_for_msg(msg_id);
121 135 if (callbacks && callbacks.iopub &&
122 136 callbacks.iopub.get_cell !== undefined) {
123 137 return callbacks.iopub.get_cell();
124 138 }
125 139 }
126 140
127 141 // Not triggered by a cell or widget (no get_cell callback
128 142 // exists).
129 143 return null;
130 144 };
131 145
132 146 WidgetManager.prototype.callbacks = function (view) {
133 147 // callback handlers specific a view
134 148 var callbacks = {};
135 149 if (view && view.options.cell) {
136 150
137 151 // Try to get output handlers
138 152 var cell = view.options.cell;
139 153 var handle_output = null;
140 154 var handle_clear_output = null;
141 155 if (cell.output_area) {
142 156 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
143 157 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
144 158 }
145 159
146 160 // Create callback dict using what is known
147 161 var that = this;
148 162 callbacks = {
149 163 iopub : {
150 164 output : handle_output,
151 165 clear_output : handle_clear_output,
152 166
153 167 // Special function only registered by widget messages.
154 168 // Allows us to get the cell for a message so we know
155 169 // where to add widgets if the code requires it.
156 170 get_cell : function () {
157 171 return cell;
158 172 },
159 173 },
160 174 };
161 175 }
162 176 return callbacks;
163 177 };
164 178
165 179 WidgetManager.prototype.get_model = function (model_id) {
166 180 // Look-up a model instance by its id.
167 181 var model = this._models[model_id];
168 182 if (model !== undefined && model.id == model_id) {
169 183 return model;
170 184 }
171 185 return null;
172 186 };
173 187
174 188 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
175 189 // Handle when a comm is opened.
176 190 var that = this;
177 191 var model_id = comm.comm_id;
178 192 var widget_type_name = msg.content.data.model_name;
179 193 var widget_model = new WidgetManager._model_types[widget_type_name](this, model_id, comm);
180 194 widget_model.on('comm:close', function () {
181 195 delete that._models[model_id];
182 196 });
183 197 this._models[model_id] = widget_model;
184 198 };
185 199
186 200 // Backwards compatability.
187 201 IPython.WidgetManager = WidgetManager;
188 202
189 203 return {'WidgetManager': WidgetManager};
190 204 });
@@ -1,610 +1,613 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define(["widgets/js/manager",
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/namespace",
9 9 ], function(widgetmanager, _, Backbone, $, IPython){
10 10
11 11 var WidgetModel = Backbone.Model.extend({
12 12 constructor: function (widget_manager, model_id, comm) {
13 13 // Constructor
14 14 //
15 15 // Creates a WidgetModel instance.
16 16 //
17 17 // Parameters
18 18 // ----------
19 19 // widget_manager : WidgetManager instance
20 20 // model_id : string
21 21 // An ID unique to this model.
22 22 // comm : Comm instance (optional)
23 23 this.widget_manager = widget_manager;
24 24 this._buffered_state_diff = {};
25 25 this.pending_msgs = 0;
26 26 this.msg_buffer = null;
27 27 this.state_lock = null;
28 28 this.id = model_id;
29 29 this.views = {};
30 30
31 31 if (comm !== undefined) {
32 32 // Remember comm associated with the model.
33 33 this.comm = comm;
34 34 comm.model = this;
35 35
36 36 // Hook comm messages up to model.
37 37 comm.on_close($.proxy(this._handle_comm_closed, this));
38 38 comm.on_msg($.proxy(this._handle_comm_msg, this));
39 39 }
40 40 return Backbone.Model.apply(this);
41 41 },
42 42
43 43 send: function (content, callbacks) {
44 44 // Send a custom msg over the comm.
45 45 if (this.comm !== undefined) {
46 46 var data = {method: 'custom', content: content};
47 47 this.comm.send(data, callbacks);
48 48 this.pending_msgs++;
49 49 }
50 50 },
51 51
52 52 _handle_comm_closed: function (msg) {
53 53 // Handle when a widget is closed.
54 54 this.trigger('comm:close');
55 55 this.stopListening();
56 56 this.trigger('destroy', this);
57 57 delete this.comm.model; // Delete ref so GC will collect widget model.
58 58 delete this.comm;
59 59 delete this.model_id; // Delete id from model so widget manager cleans up.
60 60 for (var id in this.views) {
61 61 if (this.views.hasOwnProperty(id)) {
62 62 this.views[id].remove();
63 63 }
64 64 }
65 65 },
66 66
67 67 _handle_comm_msg: function (msg) {
68 68 // Handle incoming comm msg.
69 69 var method = msg.content.data.method;
70 70 switch (method) {
71 71 case 'update':
72 72 this.set_state(msg.content.data.state);
73 73 break;
74 74 case 'custom':
75 75 this.trigger('msg:custom', msg.content.data.content);
76 76 break;
77 77 case 'display':
78 78 this.widget_manager.display_view(msg, this);
79 79 break;
80 80 }
81 81 },
82 82
83 83 set_state: function (state) {
84 84 // Handle when a widget is updated via the python side.
85 85 this.state_lock = state;
86 86 try {
87 87 var that = this;
88 88 WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
89 89 obj[key] = that._unpack_models(state[key]);
90 90 return obj;
91 91 }, {})]);
92 92 } finally {
93 93 this.state_lock = null;
94 94 }
95 95 },
96 96
97 97 _handle_status: function (msg, callbacks) {
98 98 // Handle status msgs.
99 99
100 100 // execution_state : ('busy', 'idle', 'starting')
101 101 if (this.comm !== undefined) {
102 102 if (msg.content.execution_state ==='idle') {
103 103 // Send buffer if this message caused another message to be
104 104 // throttled.
105 105 if (this.msg_buffer !== null &&
106 106 (this.get('msg_throttle') || 3) === this.pending_msgs) {
107 107 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
108 108 this.comm.send(data, callbacks);
109 109 this.msg_buffer = null;
110 110 } else {
111 111 --this.pending_msgs;
112 112 }
113 113 }
114 114 }
115 115 },
116 116
117 117 callbacks: function(view) {
118 118 // Create msg callbacks for a comm msg.
119 119 var callbacks = this.widget_manager.callbacks(view);
120 120
121 121 if (callbacks.iopub === undefined) {
122 122 callbacks.iopub = {};
123 123 }
124 124
125 125 var that = this;
126 126 callbacks.iopub.status = function (msg) {
127 127 that._handle_status(msg, callbacks);
128 128 };
129 129 return callbacks;
130 130 },
131 131
132 132 set: function(key, val, options) {
133 133 // Set a value.
134 134 var return_value = WidgetModel.__super__.set.apply(this, arguments);
135 135
136 136 // Backbone only remembers the diff of the most recent set()
137 137 // operation. Calling set multiple times in a row results in a
138 138 // loss of diff information. Here we keep our own running diff.
139 139 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
140 140 return return_value;
141 141 },
142 142
143 143 sync: function (method, model, options) {
144 144 // Handle sync to the back-end. Called when a model.save() is called.
145 145
146 146 // Make sure a comm exists.
147 147 var error = options.error || function() {
148 148 console.error('Backbone sync error:', arguments);
149 149 };
150 150 if (this.comm === undefined) {
151 151 error();
152 152 return false;
153 153 }
154 154
155 155 // Delete any key value pairs that the back-end already knows about.
156 156 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
157 157 if (this.state_lock !== null) {
158 158 var keys = Object.keys(this.state_lock);
159 159 for (var i=0; i<keys.length; i++) {
160 160 var key = keys[i];
161 161 if (attrs[key] === this.state_lock[key]) {
162 162 delete attrs[key];
163 163 }
164 164 }
165 165 }
166 166
167 167 // Only sync if there are attributes to send to the back-end.
168 168 attrs = this._pack_models(attrs);
169 169 if (_.size(attrs) > 0) {
170 170
171 171 // If this message was sent via backbone itself, it will not
172 172 // have any callbacks. It's important that we create callbacks
173 173 // so we can listen for status messages, etc...
174 174 var callbacks = options.callbacks || this.callbacks();
175 175
176 176 // Check throttle.
177 177 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
178 178 // The throttle has been exceeded, buffer the current msg so
179 179 // it can be sent once the kernel has finished processing
180 180 // some of the existing messages.
181 181
182 182 // Combine updates if it is a 'patch' sync, otherwise replace updates
183 183 switch (method) {
184 184 case 'patch':
185 185 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
186 186 break;
187 187 case 'update':
188 188 case 'create':
189 189 this.msg_buffer = attrs;
190 190 break;
191 191 default:
192 192 error();
193 193 return false;
194 194 }
195 195 this.msg_buffer_callbacks = callbacks;
196 196
197 197 } else {
198 198 // We haven't exceeded the throttle, send the message like
199 199 // normal.
200 200 var data = {method: 'backbone', sync_data: attrs};
201 201 this.comm.send(data, callbacks);
202 202 this.pending_msgs++;
203 203 }
204 204 }
205 205 // Since the comm is a one-way communication, assume the message
206 206 // arrived. Don't call success since we don't have a model back from the server
207 207 // this means we miss out on the 'sync' event.
208 208 this._buffered_state_diff = {};
209 209 },
210 210
211 211 save_changes: function(callbacks) {
212 212 // Push this model's state to the back-end
213 213 //
214 214 // This invokes a Backbone.Sync.
215 215 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
216 216 },
217 217
218 218 _pack_models: function(value) {
219 219 // Replace models with model ids recursively.
220 220 var that = this;
221 221 var packed;
222 222 if (value instanceof Backbone.Model) {
223 223 return "IPY_MODEL_" + value.id;
224 224
225 225 } else if ($.isArray(value)) {
226 226 packed = [];
227 227 _.each(value, function(sub_value, key) {
228 228 packed.push(that._pack_models(sub_value));
229 229 });
230 230 return packed;
231 231
232 232 } else if (value instanceof Object) {
233 233 packed = {};
234 234 _.each(value, function(sub_value, key) {
235 235 packed[key] = that._pack_models(sub_value);
236 236 });
237 237 return packed;
238 238
239 239 } else {
240 240 return value;
241 241 }
242 242 },
243 243
244 244 _unpack_models: function(value) {
245 245 // Replace model ids with models recursively.
246 246 var that = this;
247 247 var unpacked;
248 248 if ($.isArray(value)) {
249 249 unpacked = [];
250 250 _.each(value, function(sub_value, key) {
251 251 unpacked.push(that._unpack_models(sub_value));
252 252 });
253 253 return unpacked;
254 254
255 255 } else if (value instanceof Object) {
256 256 unpacked = {};
257 257 _.each(value, function(sub_value, key) {
258 258 unpacked[key] = that._unpack_models(sub_value);
259 259 });
260 260 return unpacked;
261 261
262 262 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
263 263 var model = this.widget_manager.get_model(value.slice(10, value.length));
264 264 if (model) {
265 265 return model;
266 266 } else {
267 267 return value;
268 268 }
269 269 } else {
270 270 return value;
271 271 }
272 272 },
273 273
274 274 on_some_change: function(keys, callback, context) {
275 275 // on_some_change(["key1", "key2"], foo, context) differs from
276 276 // on("change:key1 change:key2", foo, context).
277 277 // If the widget attributes key1 and key2 are both modified,
278 278 // the second form will result in foo being called twice
279 279 // while the first will call foo only once.
280 280 this.on('change', function() {
281 281 if (keys.some(this.hasChanged, this)) {
282 282 callback.apply(context);
283 283 }
284 284 }, this);
285 285
286 286 },
287 287 });
288 288 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
289 289
290 290
291 291 var WidgetView = Backbone.View.extend({
292 292 initialize: function(parameters) {
293 293 // Public constructor.
294 294 this.model.on('change',this.update,this);
295 295 this.options = parameters.options;
296 296 this.child_model_views = {};
297 297 this.child_views = {};
298 298 this.id = this.id || IPython.utils.uuid();
299 299 this.model.views[this.id] = this;
300 300 this.on('displayed', function() {
301 301 this.is_displayed = true;
302 302 }, this);
303 303 },
304 304
305 305 update: function(){
306 306 // Triggered on model change.
307 307 //
308 308 // Update view to be consistent with this.model
309 309 },
310 310
311 311 create_child_view: function(child_model, options) {
312 312 // Create and return a child view.
313 313 //
314 314 // -given a model and (optionally) a view name if the view name is
315 315 // not given, it defaults to the model's default view attribute.
316 316
317 317 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
318 318 // it would be great to have the widget manager add the cell metadata
319 319 // to the subview without having to add it here.
320 options = $.extend({ parent: this }, options || {});
321 var child_view = this.model.widget_manager.create_view(child_model, options, this);
322
323 // Associate the view id with the model id.
324 if (this.child_model_views[child_model.id] === undefined) {
325 this.child_model_views[child_model.id] = [];
326 }
327 this.child_model_views[child_model.id].push(child_view.id);
320 var that = this;
321 var old_callback = options.callback || function(view) {};
322 options = $.extend({ parent: this, callback: function(child_view) {
323 // Associate the view id with the model id.
324 if (that.child_model_views[child_model.id] === undefined) {
325 that.child_model_views[child_model.id] = [];
326 }
327 that.child_model_views[child_model.id].push(child_view.id);
328 328
329 // Remember the view by id.
330 this.child_views[child_view.id] = child_view;
331 return child_view;
329 // Remember the view by id.
330 that.child_views[child_view.id] = child_view;
331 old_callback(child_view);
332 }}, options || {});
333
334 this.model.widget_manager.create_view(child_model, options);
332 335 },
333 336
334 337 pop_child_view: function(child_model) {
335 338 // Delete a child view that was previously created using create_child_view.
336 339 var view_ids = this.child_model_views[child_model.id];
337 340 if (view_ids !== undefined) {
338 341
339 342 // Only delete the first view in the list.
340 343 var view_id = view_ids[0];
341 344 var view = this.child_views[view_id];
342 345 delete this.child_views[view_id];
343 346 view_ids.splice(0,1);
344 347 delete child_model.views[view_id];
345 348
346 349 // Remove the view list specific to this model if it is empty.
347 350 if (view_ids.length === 0) {
348 351 delete this.child_model_views[child_model.id];
349 352 }
350 353 return view;
351 354 }
352 355 return null;
353 356 },
354 357
355 358 do_diff: function(old_list, new_list, removed_callback, added_callback) {
356 359 // Difference a changed list and call remove and add callbacks for
357 360 // each removed and added item in the new list.
358 361 //
359 362 // Parameters
360 363 // ----------
361 364 // old_list : array
362 365 // new_list : array
363 366 // removed_callback : Callback(item)
364 367 // Callback that is called for each item removed.
365 368 // added_callback : Callback(item)
366 369 // Callback that is called for each item added.
367 370
368 371 // Walk the lists until an unequal entry is found.
369 372 var i;
370 373 for (i = 0; i < new_list.length; i++) {
371 374 if (i >= old_list.length || new_list[i] !== old_list[i]) {
372 375 break;
373 376 }
374 377 }
375 378
376 379 // Remove the non-matching items from the old list.
377 380 for (var j = i; j < old_list.length; j++) {
378 381 removed_callback(old_list[j]);
379 382 }
380 383
381 384 // Add the rest of the new list items.
382 385 for (; i < new_list.length; i++) {
383 386 added_callback(new_list[i]);
384 387 }
385 388 },
386 389
387 390 callbacks: function(){
388 391 // Create msg callbacks for a comm msg.
389 392 return this.model.callbacks(this);
390 393 },
391 394
392 395 render: function(){
393 396 // Render the view.
394 397 //
395 398 // By default, this is only called the first time the view is created
396 399 },
397 400
398 401 show: function(){
399 402 // Show the widget-area
400 403 if (this.options && this.options.cell &&
401 404 this.options.cell.widget_area !== undefined) {
402 405 this.options.cell.widget_area.show();
403 406 }
404 407 },
405 408
406 409 send: function (content) {
407 410 // Send a custom msg associated with this view.
408 411 this.model.send(content, this.callbacks());
409 412 },
410 413
411 414 touch: function () {
412 415 this.model.save_changes(this.callbacks());
413 416 },
414 417
415 418 after_displayed: function (callback, context) {
416 419 // Calls the callback right away is the view is already displayed
417 420 // otherwise, register the callback to the 'displayed' event.
418 421 if (this.is_displayed) {
419 422 callback.apply(context);
420 423 } else {
421 424 this.on('displayed', callback, context);
422 425 }
423 426 },
424 427 });
425 428
426 429
427 430 var DOMWidgetView = WidgetView.extend({
428 431 initialize: function (parameters) {
429 432 // Public constructor
430 433 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
431 434 this.on('displayed', this.show, this);
432 435 this.model.on('change:visible', this.update_visible, this);
433 436 this.model.on('change:_css', this.update_css, this);
434 437
435 438 this.model.on('change:_dom_classes', function(model, new_classes) {
436 439 var old_classes = model.previous('_dom_classes');
437 440 this.update_classes(old_classes, new_classes);
438 441 }, this);
439 442
440 443 this.model.on('change:color', function (model, value) {
441 444 this.update_attr('color', value); }, this);
442 445
443 446 this.model.on('change:background_color', function (model, value) {
444 447 this.update_attr('background', value); }, this);
445 448
446 449 this.model.on('change:width', function (model, value) {
447 450 this.update_attr('width', value); }, this);
448 451
449 452 this.model.on('change:height', function (model, value) {
450 453 this.update_attr('height', value); }, this);
451 454
452 455 this.model.on('change:border_color', function (model, value) {
453 456 this.update_attr('border-color', value); }, this);
454 457
455 458 this.model.on('change:border_width', function (model, value) {
456 459 this.update_attr('border-width', value); }, this);
457 460
458 461 this.model.on('change:border_style', function (model, value) {
459 462 this.update_attr('border-style', value); }, this);
460 463
461 464 this.model.on('change:font_style', function (model, value) {
462 465 this.update_attr('font-style', value); }, this);
463 466
464 467 this.model.on('change:font_weight', function (model, value) {
465 468 this.update_attr('font-weight', value); }, this);
466 469
467 470 this.model.on('change:font_size', function (model, value) {
468 471 this.update_attr('font-size', this._default_px(value)); }, this);
469 472
470 473 this.model.on('change:font_family', function (model, value) {
471 474 this.update_attr('font-family', value); }, this);
472 475
473 476 this.model.on('change:padding', function (model, value) {
474 477 this.update_attr('padding', value); }, this);
475 478
476 479 this.model.on('change:margin', function (model, value) {
477 480 this.update_attr('margin', this._default_px(value)); }, this);
478 481
479 482 this.model.on('change:border_radius', function (model, value) {
480 483 this.update_attr('border-radius', this._default_px(value)); }, this);
481 484
482 485 this.after_displayed(function() {
483 486 this.update_visible(this.model, this.model.get("visible"));
484 487 this.update_classes([], this.model.get('_dom_classes'));
485 488
486 489 this.update_attr('color', this.model.get('color'));
487 490 this.update_attr('background', this.model.get('background_color'));
488 491 this.update_attr('width', this.model.get('width'));
489 492 this.update_attr('height', this.model.get('height'));
490 493 this.update_attr('border-color', this.model.get('border_color'));
491 494 this.update_attr('border-width', this.model.get('border_width'));
492 495 this.update_attr('border-style', this.model.get('border_style'));
493 496 this.update_attr('font-style', this.model.get('font_style'));
494 497 this.update_attr('font-weight', this.model.get('font_weight'));
495 498 this.update_attr('font-size', this.model.get('font_size'));
496 499 this.update_attr('font-family', this.model.get('font_family'));
497 500 this.update_attr('padding', this.model.get('padding'));
498 501 this.update_attr('margin', this.model.get('margin'));
499 502 this.update_attr('border-radius', this.model.get('border_radius'));
500 503
501 504 this.update_css(this.model, this.model.get("_css"));
502 505 }, this);
503 506 },
504 507
505 508 _default_px: function(value) {
506 509 // Makes browser interpret a numerical string as a pixel value.
507 510 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
508 511 return value.trim() + 'px';
509 512 }
510 513 return value;
511 514 },
512 515
513 516 update_attr: function(name, value) {
514 517 // Set a css attr of the widget view.
515 518 this.$el.css(name, value);
516 519 },
517 520
518 521 update_visible: function(model, value) {
519 522 // Update visibility
520 523 this.$el.toggle(value);
521 524 },
522 525
523 526 update_css: function (model, css) {
524 527 // Update the css styling of this view.
525 528 var e = this.$el;
526 529 if (css === undefined) {return;}
527 530 for (var i = 0; i < css.length; i++) {
528 531 // Apply the css traits to all elements that match the selector.
529 532 var selector = css[i][0];
530 533 var elements = this._get_selector_element(selector);
531 534 if (elements.length > 0) {
532 535 var trait_key = css[i][1];
533 536 var trait_value = css[i][2];
534 537 elements.css(trait_key ,trait_value);
535 538 }
536 539 }
537 540 },
538 541
539 542 update_classes: function (old_classes, new_classes, $el) {
540 543 // Update the DOM classes applied to an element, default to this.$el.
541 544 if ($el===undefined) {
542 545 $el = this.$el;
543 546 }
544 547 this.do_diff(old_classes, new_classes, function(removed) {
545 548 $el.removeClass(removed);
546 549 }, function(added) {
547 550 $el.addClass(added);
548 551 });
549 552 },
550 553
551 554 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
552 555 // Update the DOM classes applied to the widget based on a single
553 556 // trait's value.
554 557 //
555 558 // Given a trait value classes map, this function automatically
556 559 // handles applying the appropriate classes to the widget element
557 560 // and removing classes that are no longer valid.
558 561 //
559 562 // Parameters
560 563 // ----------
561 564 // class_map: dictionary
562 565 // Dictionary of trait values to class lists.
563 566 // Example:
564 567 // {
565 568 // success: ['alert', 'alert-success'],
566 569 // info: ['alert', 'alert-info'],
567 570 // warning: ['alert', 'alert-warning'],
568 571 // danger: ['alert', 'alert-danger']
569 572 // };
570 573 // trait_name: string
571 574 // Name of the trait to check the value of.
572 575 // previous_trait_value: optional string, default ''
573 576 // Last trait value
574 577 // $el: optional jQuery element handle, defaults to this.$el
575 578 // Element that the classes are applied to.
576 579 var key = previous_trait_value;
577 580 if (key === undefined) {
578 581 key = this.model.previous(trait_name);
579 582 }
580 583 var old_classes = class_map[key] ? class_map[key] : [];
581 584 key = this.model.get(trait_name);
582 585 var new_classes = class_map[key] ? class_map[key] : [];
583 586
584 587 this.update_classes(old_classes, new_classes, $el || this.$el);
585 588 },
586 589
587 590 _get_selector_element: function (selector) {
588 591 // Get the elements via the css selector.
589 592 var elements;
590 593 if (!selector) {
591 594 elements = this.$el;
592 595 } else {
593 596 elements = this.$el.find(selector).addBack(selector);
594 597 }
595 598 return elements;
596 599 },
597 600 });
598 601
599 602
600 603 var widget = {
601 604 'WidgetModel': WidgetModel,
602 605 'WidgetView': WidgetView,
603 606 'DOMWidgetView': DOMWidgetView,
604 607 };
605 608
606 609 // For backwards compatability.
607 610 $.extend(IPython, widget);
608 611
609 612 return widget;
610 613 });
@@ -1,344 +1,346 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "widgets/js/widget",
6 6 "jqueryui",
7 7 "bootstrap",
8 8 ], function(widget, $){
9 9
10 10 var BoxView = widget.DOMWidgetView.extend({
11 11 initialize: function(){
12 12 // Public constructor
13 13 BoxView.__super__.initialize.apply(this, arguments);
14 14 this.model.on('change:children', function(model, value) {
15 15 this.update_children(model.previous('children'), value);
16 16 }, this);
17 17 this.model.on('change:overflow_x', function(model, value) {
18 18 this.update_overflow_x();
19 19 }, this);
20 20 this.model.on('change:overflow_y', function(model, value) {
21 21 this.update_overflow_y();
22 22 }, this);
23 23 this.model.on('change:box_style', function(model, value) {
24 24 this.update_box_style();
25 25 }, this);
26 26 },
27 27
28 28 update_attr: function(name, value) {
29 29 // Set a css attr of the widget view.
30 30 this.$box.css(name, value);
31 31 },
32 32
33 33 render: function(){
34 34 // Called when view is rendered.
35 35 this.$box = this.$el;
36 36 this.$box.addClass('widget-box');
37 37 this.update_children([], this.model.get('children'));
38 38 this.update_overflow_x();
39 39 this.update_overflow_y();
40 40 this.update_box_style('');
41 41 },
42 42
43 43 update_overflow_x: function() {
44 44 // Called when the x-axis overflow setting is changed.
45 45 this.$box.css('overflow-x', this.model.get('overflow_x'));
46 46 },
47 47
48 48 update_overflow_y: function() {
49 49 // Called when the y-axis overflow setting is changed.
50 50 this.$box.css('overflow-y', this.model.get('overflow_y'));
51 51 },
52 52
53 53 update_box_style: function(previous_trait_value) {
54 54 var class_map = {
55 55 success: ['alert', 'alert-success'],
56 56 info: ['alert', 'alert-info'],
57 57 warning: ['alert', 'alert-warning'],
58 58 danger: ['alert', 'alert-danger']
59 59 };
60 60 this.update_mapped_classes(class_map, 'box_style', previous_trait_value, this.$box);
61 61 },
62 62
63 63 update_children: function(old_list, new_list) {
64 64 // Called when the children list changes.
65 65 this.do_diff(old_list, new_list,
66 66 $.proxy(this.remove_child_model, this),
67 67 $.proxy(this.add_child_model, this));
68 68 },
69 69
70 70 remove_child_model: function(model) {
71 71 // Called when a model is removed from the children list.
72 72 this.pop_child_view(model).remove();
73 73 },
74 74
75 75 add_child_model: function(model) {
76 76 // Called when a model is added to the children list.
77 var view = this.create_child_view(model);
78 this.$box.append(view.$el);
77 var that = this;
78 this.create_child_view(model, {callback: function(view) {
79 that.$box.append(view.$el);
79 80
80 // Trigger the displayed event of the child view.
81 this.after_displayed(function() {
82 view.trigger('displayed');
83 });
81 // Trigger the displayed event of the child view.
82 that.after_displayed(function() {
83 view.trigger('displayed');
84 });
85 }});
84 86 },
85 87 });
86 88
87 89
88 90 var FlexBoxView = BoxView.extend({
89 91 render: function(){
90 92 FlexBoxView.__super__.render.apply(this);
91 93 this.model.on('change:orientation', this.update_orientation, this);
92 94 this.model.on('change:flex', this._flex_changed, this);
93 95 this.model.on('change:pack', this._pack_changed, this);
94 96 this.model.on('change:align', this._align_changed, this);
95 97 this._flex_changed();
96 98 this._pack_changed();
97 99 this._align_changed();
98 100 this.update_orientation();
99 101 },
100 102
101 103 update_orientation: function(){
102 104 var orientation = this.model.get("orientation");
103 105 if (orientation == "vertical") {
104 106 this.$box.removeClass("hbox").addClass("vbox");
105 107 } else {
106 108 this.$box.removeClass("vbox").addClass("hbox");
107 109 }
108 110 },
109 111
110 112 _flex_changed: function(){
111 113 if (this.model.previous('flex')) {
112 114 this.$box.removeClass('box-flex' + this.model.previous('flex'));
113 115 }
114 116 this.$box.addClass('box-flex' + this.model.get('flex'));
115 117 },
116 118
117 119 _pack_changed: function(){
118 120 if (this.model.previous('pack')) {
119 121 this.$box.removeClass(this.model.previous('pack'));
120 122 }
121 123 this.$box.addClass(this.model.get('pack'));
122 124 },
123 125
124 126 _align_changed: function(){
125 127 if (this.model.previous('align')) {
126 128 this.$box.removeClass('align-' + this.model.previous('align'));
127 129 }
128 130 this.$box.addClass('align-' + this.model.get('align'));
129 131 },
130 132 });
131 133
132 134 var PopupView = BoxView.extend({
133 135
134 136 render: function(){
135 137 // Called when view is rendered.
136 138 var that = this;
137 139
138 140 this.$el.on("remove", function(){
139 141 that.$backdrop.remove();
140 142 });
141 143 this.$backdrop = $('<div />')
142 144 .appendTo($('#notebook-container'))
143 145 .addClass('modal-dialog')
144 146 .css('position', 'absolute')
145 147 .css('left', '0px')
146 148 .css('top', '0px');
147 149 this.$window = $('<div />')
148 150 .appendTo(this.$backdrop)
149 151 .addClass('modal-content widget-modal')
150 152 .mousedown(function(){
151 153 that.bring_to_front();
152 154 });
153 155
154 156 // Set the elements array since the this.$window element is not child
155 157 // of this.$el and the parent widget manager or other widgets may
156 158 // need to know about all of the top-level widgets. The IPython
157 159 // widget manager uses this to register the elements with the
158 160 // keyboard manager.
159 161 this.additional_elements = [this.$window];
160 162
161 163 this.$title_bar = $('<div />')
162 164 .addClass('popover-title')
163 165 .appendTo(this.$window)
164 166 .mousedown(function(){
165 167 that.bring_to_front();
166 168 });
167 169 this.$close = $('<button />')
168 170 .addClass('close fa fa-remove')
169 171 .css('margin-left', '5px')
170 172 .appendTo(this.$title_bar)
171 173 .click(function(){
172 174 that.hide();
173 175 event.stopPropagation();
174 176 });
175 177 this.$minimize = $('<button />')
176 178 .addClass('close fa fa-arrow-down')
177 179 .appendTo(this.$title_bar)
178 180 .click(function(){
179 181 that.popped_out = !that.popped_out;
180 182 if (!that.popped_out) {
181 183 that.$minimize
182 184 .removeClass('fa-arrow-down')
183 185 .addClass('fa-arrow-up');
184 186
185 187 that.$window
186 188 .draggable('destroy')
187 189 .resizable('destroy')
188 190 .removeClass('widget-modal modal-content')
189 191 .addClass('docked-widget-modal')
190 192 .detach()
191 193 .insertBefore(that.$show_button);
192 194 that.$show_button.hide();
193 195 that.$close.hide();
194 196 } else {
195 197 that.$minimize
196 198 .addClass('fa-arrow-down')
197 199 .removeClass('fa-arrow-up');
198 200
199 201 that.$window
200 202 .removeClass('docked-widget-modal')
201 203 .addClass('widget-modal modal-content')
202 204 .detach()
203 205 .appendTo(that.$backdrop)
204 206 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
205 207 .resizable()
206 208 .children('.ui-resizable-handle').show();
207 209 that.show();
208 210 that.$show_button.show();
209 211 that.$close.show();
210 212 }
211 213 event.stopPropagation();
212 214 });
213 215 this.$title = $('<div />')
214 216 .addClass('widget-modal-title')
215 217 .html("&nbsp;")
216 218 .appendTo(this.$title_bar);
217 219 this.$box = $('<div />')
218 220 .addClass('modal-body')
219 221 .addClass('widget-modal-body')
220 222 .addClass('widget-box')
221 223 .addClass('vbox')
222 224 .appendTo(this.$window);
223 225
224 226 this.$show_button = $('<button />')
225 227 .html("&nbsp;")
226 228 .addClass('btn btn-info widget-modal-show')
227 229 .appendTo(this.$el)
228 230 .click(function(){
229 231 that.show();
230 232 });
231 233
232 234 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
233 235 this.$window.resizable();
234 236 this.$window.on('resize', function(){
235 237 that.$box.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
236 238 });
237 239
238 240 this._shown_once = false;
239 241 this.popped_out = true;
240 242
241 243 this.update_children([], this.model.get('children'));
242 244 this.model.on('change:children', function(model, value) {
243 245 this.update_children(model.previous('children'), value);
244 246 }, this);
245 247 },
246 248
247 249 hide: function() {
248 250 // Called when the modal hide button is clicked.
249 251 this.$window.hide();
250 252 this.$show_button.removeClass('btn-info');
251 253 },
252 254
253 255 show: function() {
254 256 // Called when the modal show button is clicked.
255 257 this.$show_button.addClass('btn-info');
256 258 this.$window.show();
257 259 if (this.popped_out) {
258 260 this.$window.css("positon", "absolute");
259 261 this.$window.css("top", "0px");
260 262 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
261 263 $(window).scrollLeft()) + "px");
262 264 this.bring_to_front();
263 265 }
264 266 },
265 267
266 268 bring_to_front: function() {
267 269 // Make the modal top-most, z-ordered about the other modals.
268 270 var $widget_modals = $(".widget-modal");
269 271 var max_zindex = 0;
270 272 $widget_modals.each(function (index, el){
271 273 var zindex = parseInt($(el).css('z-index'));
272 274 if (!isNaN(zindex)) {
273 275 max_zindex = Math.max(max_zindex, zindex);
274 276 }
275 277 });
276 278
277 279 // Start z-index of widget modals at 2000
278 280 max_zindex = Math.max(max_zindex, 2000);
279 281
280 282 $widget_modals.each(function (index, el){
281 283 $el = $(el);
282 284 if (max_zindex == parseInt($el.css('z-index'))) {
283 285 $el.css('z-index', max_zindex - 1);
284 286 }
285 287 });
286 288 this.$window.css('z-index', max_zindex);
287 289 },
288 290
289 291 update: function(){
290 292 // Update the contents of this view
291 293 //
292 294 // Called when the model is changed. The model may have been
293 295 // changed by another view or by a state update from the back-end.
294 296 var description = this.model.get('description');
295 297 if (description.trim().length === 0) {
296 298 this.$title.html("&nbsp;"); // Preserve title height
297 299 } else {
298 300 this.$title.text(description);
299 301 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
300 302 }
301 303
302 304 var button_text = this.model.get('button_text');
303 305 if (button_text.trim().length === 0) {
304 306 this.$show_button.html("&nbsp;"); // Preserve button height
305 307 } else {
306 308 this.$show_button.text(button_text);
307 309 }
308 310
309 311 if (!this._shown_once) {
310 312 this._shown_once = true;
311 313 this.show();
312 314 }
313 315
314 316 return PopupView.__super__.update.apply(this);
315 317 },
316 318
317 319 _get_selector_element: function(selector) {
318 320 // Get an element view a 'special' jquery selector. (see widget.js)
319 321 //
320 322 // Since the modal actually isn't within the $el in the DOM, we need to extend
321 323 // the selector logic to allow the user to set css on the modal if need be.
322 324 // The convention used is:
323 325 // "modal" - select the modal div
324 326 // "modal [selector]" - select element(s) within the modal div.
325 327 // "[selector]" - select elements within $el
326 328 // "" - select the $el
327 329 if (selector.substring(0, 5) == 'modal') {
328 330 if (selector == 'modal') {
329 331 return this.$window;
330 332 } else {
331 333 return this.$window.find(selector.substring(6));
332 334 }
333 335 } else {
334 336 return PopupView.__super__._get_selector_element.apply(this, [selector]);
335 337 }
336 338 },
337 339 });
338 340
339 341 return {
340 342 'BoxView': BoxView,
341 343 'PopupView': PopupView,
342 344 'FlexBoxView': FlexBoxView,
343 345 };
344 346 });
@@ -1,253 +1,257 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "widgets/js/widget",
6 6 "base/js/utils",
7 7 "jquery",
8 8 "bootstrap",
9 9 ], function(widget, utils, $){
10 10
11 11 var AccordionView = widget.DOMWidgetView.extend({
12 12 render: function(){
13 13 // Called when view is rendered.
14 14 var guid = 'panel-group' + utils.uuid();
15 15 this.$el
16 16 .attr('id', guid)
17 17 .addClass('panel-group');
18 18 this.containers = [];
19 19 this.model_containers = {};
20 20 this.update_children([], this.model.get('children'));
21 21 this.model.on('change:children', function(model, value, options) {
22 22 this.update_children(model.previous('children'), value);
23 23 }, this);
24 24 this.model.on('change:selected_index', function(model, value, options) {
25 25 this.update_selected_index(model.previous('selected_index'), value, options);
26 26 }, this);
27 27 this.model.on('change:_titles', function(model, value, options) {
28 28 this.update_titles(value);
29 29 }, this);
30 30 var that = this;
31 31 this.on('displayed', function() {
32 32 this.update_titles();
33 33 }, this);
34 34 },
35 35
36 36 update_titles: function(titles) {
37 37 // Set tab titles
38 38 if (!titles) {
39 39 titles = this.model.get('_titles');
40 40 }
41 41
42 42 var that = this;
43 43 _.each(titles, function(title, page_index) {
44 44 var accordian = that.containers[page_index];
45 45 if (accordian !== undefined) {
46 46 accordian
47 47 .find('.panel-heading')
48 48 .find('.accordion-toggle')
49 49 .text(title);
50 50 }
51 51 });
52 52 },
53 53
54 54 update_selected_index: function(old_index, new_index, options) {
55 55 // Only update the selection if the selection wasn't triggered
56 56 // by the front-end. It must be triggered by the back-end.
57 57 if (options === undefined || options.updated_view != this) {
58 58 this.containers[old_index].find('.panel-collapse').collapse('hide');
59 59 if (0 <= new_index && new_index < this.containers.length) {
60 60 this.containers[new_index].find('.panel-collapse').collapse('show');
61 61 }
62 62 }
63 63 },
64 64
65 65 update_children: function(old_list, new_list) {
66 66 // Called when the children list is modified.
67 67 this.do_diff(old_list,
68 68 new_list,
69 69 $.proxy(this.remove_child_model, this),
70 70 $.proxy(this.add_child_model, this));
71 71 },
72 72
73 73 remove_child_model: function(model) {
74 74 // Called when a child is removed from children list.
75 75 var accordion_group = this.model_containers[model.id];
76 76 this.containers.splice(accordion_group.container_index, 1);
77 77 delete this.model_containers[model.id];
78 78 accordion_group.remove();
79 79 this.pop_child_view(model);
80 80 },
81 81
82 82 add_child_model: function(model) {
83 83 // Called when a child is added to children list.
84 var view = this.create_child_view(model);
85 84 var index = this.containers.length;
86 85 var uuid = utils.uuid();
87 86 var accordion_group = $('<div />')
88 87 .addClass('panel panel-default')
89 88 .appendTo(this.$el);
90 89 var accordion_heading = $('<div />')
91 90 .addClass('panel-heading')
92 91 .appendTo(accordion_group);
93 92 var that = this;
94 93 var accordion_toggle = $('<a />')
95 94 .addClass('accordion-toggle')
96 95 .attr('data-toggle', 'collapse')
97 96 .attr('data-parent', '#' + this.$el.attr('id'))
98 97 .attr('href', '#' + uuid)
99 98 .click(function(evt){
100 99
101 100 // Calling model.set will trigger all of the other views of the
102 101 // model to update.
103 102 that.model.set("selected_index", index, {updated_view: that});
104 103 that.touch();
105 104 })
106 105 .text('Page ' + index)
107 106 .appendTo(accordion_heading);
108 107 var accordion_body = $('<div />', {id: uuid})
109 108 .addClass('panel-collapse collapse')
110 109 .appendTo(accordion_group);
111 110 var accordion_inner = $('<div />')
112 111 .addClass('panel-body')
113 112 .appendTo(accordion_body);
114 113 var container_index = this.containers.push(accordion_group) - 1;
115 114 accordion_group.container_index = container_index;
116 115 this.model_containers[model.id] = accordion_group;
117 accordion_inner.append(view.$el);
116
117 this.create_child_view(model, {callback: function(view) {
118 accordion_inner.append(view.$el);
118 119
119 this.update();
120 this.update_titles();
120 that.update();
121 that.update_titles();
121 122
122 // Trigger the displayed event of the child view.
123 this.after_displayed(function() {
124 view.trigger('displayed');
125 });
123 // Trigger the displayed event of the child view.
124 that.after_displayed(function() {
125 view.trigger('displayed');
126 });
127 }});
126 128 },
127 129 });
128 130
129 131
130 132 var TabView = widget.DOMWidgetView.extend({
131 133 initialize: function() {
132 134 // Public constructor.
133 135 this.containers = [];
134 136 TabView.__super__.initialize.apply(this, arguments);
135 137 },
136 138
137 139 render: function(){
138 140 // Called when view is rendered.
139 141 var uuid = 'tabs'+utils.uuid();
140 142 var that = this;
141 143 this.$tabs = $('<div />', {id: uuid})
142 144 .addClass('nav')
143 145 .addClass('nav-tabs')
144 146 .appendTo(this.$el);
145 147 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
146 148 .addClass('tab-content')
147 149 .appendTo(this.$el);
148 150 this.containers = [];
149 151 this.update_children([], this.model.get('children'));
150 152 this.model.on('change:children', function(model, value, options) {
151 153 this.update_children(model.previous('children'), value);
152 154 }, this);
153 155 },
154 156
155 157 update_attr: function(name, value) {
156 158 // Set a css attr of the widget view.
157 159 this.$tabs.css(name, value);
158 160 },
159 161
160 162 update_children: function(old_list, new_list) {
161 163 // Called when the children list is modified.
162 164 this.do_diff(old_list,
163 165 new_list,
164 166 $.proxy(this.remove_child_model, this),
165 167 $.proxy(this.add_child_model, this));
166 168 },
167 169
168 170 remove_child_model: function(model) {
169 171 // Called when a child is removed from children list.
170 172 var view = this.pop_child_view(model);
171 173 this.containers.splice(view.parent_tab.tab_text_index, 1);
172 174 view.parent_tab.remove();
173 175 view.parent_container.remove();
174 176 view.remove();
175 177 },
176 178
177 179 add_child_model: function(model) {
178 180 // Called when a child is added to children list.
179 var view = this.create_child_view(model);
180 181 var index = this.containers.length;
181 182 var uuid = utils.uuid();
182 183
183 184 var that = this;
184 185 var tab = $('<li />')
185 186 .css('list-style-type', 'none')
186 187 .appendTo(this.$tabs);
187 view.parent_tab = tab;
188
189 var tab_text = $('<a />')
190 .attr('href', '#' + uuid)
191 .attr('data-toggle', 'tab')
192 .text('Page ' + index)
193 .appendTo(tab)
194 .click(function (e) {
195 188
196 // Calling model.set will trigger all of the other views of the
197 // model to update.
198 that.model.set("selected_index", index, {updated_view: this});
199 that.touch();
200 that.select_page(index);
189 this.create_child_view(model, {callback: function(view) {
190 view.parent_tab = tab;
191
192 var tab_text = $('<a />')
193 .attr('href', '#' + uuid)
194 .attr('data-toggle', 'tab')
195 .text('Page ' + index)
196 .appendTo(tab)
197 .click(function (e) {
198
199 // Calling model.set will trigger all of the other views of the
200 // model to update.
201 that.model.set("selected_index", index, {updated_view: that});
202 that.touch();
203 that.select_page(index);
204 });
205 tab.tab_text_index = that.containers.push(tab_text) - 1;
206
207 var contents_div = $('<div />', {id: uuid})
208 .addClass('tab-pane')
209 .addClass('fade')
210 .append(view.$el)
211 .appendTo(that.$tab_contents);
212 view.parent_container = contents_div;
213
214 // Trigger the displayed event of the child view.
215 that.after_displayed(function() {
216 view.trigger('displayed');
201 217 });
202 tab.tab_text_index = this.containers.push(tab_text) - 1;
203
204 var contents_div = $('<div />', {id: uuid})
205 .addClass('tab-pane')
206 .addClass('fade')
207 .append(view.$el)
208 .appendTo(this.$tab_contents);
209 view.parent_container = contents_div;
210
211 // Trigger the displayed event of the child view.
212 this.after_displayed(function() {
213 view.trigger('displayed');
214 });
218 }});
215 219 },
216 220
217 221 update: function(options) {
218 222 // Update the contents of this view
219 223 //
220 224 // Called when the model is changed. The model may have been
221 225 // changed by another view or by a state update from the back-end.
222 226 if (options === undefined || options.updated_view != this) {
223 227 // Set tab titles
224 228 var titles = this.model.get('_titles');
225 229 var that = this;
226 230 _.each(titles, function(title, page_index) {
227 231 var tab_text = that.containers[page_index];
228 232 if (tab_text !== undefined) {
229 233 tab_text.text(title);
230 234 }
231 235 });
232 236
233 237 var selected_index = this.model.get('selected_index');
234 238 if (0 <= selected_index && selected_index < this.containers.length) {
235 239 this.select_page(selected_index);
236 240 }
237 241 }
238 242 return TabView.__super__.update.apply(this);
239 243 },
240 244
241 245 select_page: function(index) {
242 246 // Select a page.
243 247 this.$tabs.find('li')
244 248 .removeClass('active');
245 249 this.containers[index].tab('show');
246 250 },
247 251 });
248 252
249 253 return {
250 254 'AccordionView': AccordionView,
251 255 'TabView': TabView,
252 256 };
253 257 });
@@ -1,448 +1,450 b''
1 1 """Base Widget class. Allows user to create widgets in the back-end that render
2 2 in the IPython notebook front-end.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15 from contextlib import contextmanager
16 16 import collections
17 17
18 18 from IPython.core.getipython import get_ipython
19 19 from IPython.kernel.comm import Comm
20 20 from IPython.config import LoggingConfigurable
21 21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
22 22 CaselessStrEnum, Tuple, CUnicode, Int, Set
23 23 from IPython.utils.py3compat import string_types
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Classes
27 27 #-----------------------------------------------------------------------------
28 28 class CallbackDispatcher(LoggingConfigurable):
29 29 """A structure for registering and running callbacks"""
30 30 callbacks = List()
31 31
32 32 def __call__(self, *args, **kwargs):
33 33 """Call all of the registered callbacks."""
34 34 value = None
35 35 for callback in self.callbacks:
36 36 try:
37 37 local_value = callback(*args, **kwargs)
38 38 except Exception as e:
39 39 ip = get_ipython()
40 40 if ip is None:
41 41 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
42 42 else:
43 43 ip.showtraceback()
44 44 else:
45 45 value = local_value if local_value is not None else value
46 46 return value
47 47
48 48 def register_callback(self, callback, remove=False):
49 49 """(Un)Register a callback
50 50
51 51 Parameters
52 52 ----------
53 53 callback: method handle
54 54 Method to be registered or unregistered.
55 55 remove=False: bool
56 56 Whether to unregister the callback."""
57 57
58 58 # (Un)Register the callback.
59 59 if remove and callback in self.callbacks:
60 60 self.callbacks.remove(callback)
61 61 elif not remove and callback not in self.callbacks:
62 62 self.callbacks.append(callback)
63 63
64 64 def _show_traceback(method):
65 65 """decorator for showing tracebacks in IPython"""
66 66 def m(self, *args, **kwargs):
67 67 try:
68 68 return(method(self, *args, **kwargs))
69 69 except Exception as e:
70 70 ip = get_ipython()
71 71 if ip is None:
72 72 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
73 73 else:
74 74 ip.showtraceback()
75 75 return m
76 76
77 77 class Widget(LoggingConfigurable):
78 78 #-------------------------------------------------------------------------
79 79 # Class attributes
80 80 #-------------------------------------------------------------------------
81 81 _widget_construction_callback = None
82 82 widgets = {}
83 83
84 84 @staticmethod
85 85 def on_widget_constructed(callback):
86 86 """Registers a callback to be called when a widget is constructed.
87 87
88 88 The callback must have the following signature:
89 89 callback(widget)"""
90 90 Widget._widget_construction_callback = callback
91 91
92 92 @staticmethod
93 93 def _call_widget_constructed(widget):
94 94 """Static method, called when a widget is constructed."""
95 95 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
96 96 Widget._widget_construction_callback(widget)
97 97
98 98 #-------------------------------------------------------------------------
99 99 # Traits
100 100 #-------------------------------------------------------------------------
101 101 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
102 102 registered in the front-end to create and sync this widget with.""")
103 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
104 If empty, look in the global registry.""", sync=True)
103 105 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
104 106 to use to represent the widget.""", sync=True)
105 107 comm = Instance('IPython.kernel.comm.Comm')
106 108
107 109 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
108 110 front-end can send before receiving an idle msg from the back-end.""")
109 111
110 112 version = Int(0, sync=True, help="""Widget's version""")
111 113 keys = List()
112 114 def _keys_default(self):
113 115 return [name for name in self.traits(sync=True)]
114 116
115 117 _property_lock = Tuple((None, None))
116 118 _send_state_lock = Int(0)
117 119 _states_to_send = Set(allow_none=False)
118 120 _display_callbacks = Instance(CallbackDispatcher, ())
119 121 _msg_callbacks = Instance(CallbackDispatcher, ())
120 122
121 123 #-------------------------------------------------------------------------
122 124 # (Con/de)structor
123 125 #-------------------------------------------------------------------------
124 126 def __init__(self, **kwargs):
125 127 """Public constructor"""
126 128 self._model_id = kwargs.pop('model_id', None)
127 129 super(Widget, self).__init__(**kwargs)
128 130
129 131 Widget._call_widget_constructed(self)
130 132 self.open()
131 133
132 134 def __del__(self):
133 135 """Object disposal"""
134 136 self.close()
135 137
136 138 #-------------------------------------------------------------------------
137 139 # Properties
138 140 #-------------------------------------------------------------------------
139 141
140 142 def open(self):
141 143 """Open a comm to the frontend if one isn't already open."""
142 144 if self.comm is None:
143 145 args = dict(target_name='ipython.widget', data={ 'model_name': self._model_name })
144 146 if self._model_id is not None:
145 147 args['comm_id'] = self._model_id
146 148 self.comm = Comm(**args)
147 149 self._model_id = self.model_id
148 150
149 151 self.comm.on_msg(self._handle_msg)
150 152 Widget.widgets[self.model_id] = self
151 153
152 154 # first update
153 155 self.send_state()
154 156
155 157 @property
156 158 def model_id(self):
157 159 """Gets the model id of this widget.
158 160
159 161 If a Comm doesn't exist yet, a Comm will be created automagically."""
160 162 return self.comm.comm_id
161 163
162 164 #-------------------------------------------------------------------------
163 165 # Methods
164 166 #-------------------------------------------------------------------------
165 167
166 168 def close(self):
167 169 """Close method.
168 170
169 171 Closes the underlying comm.
170 172 When the comm is closed, all of the widget views are automatically
171 173 removed from the front-end."""
172 174 if self.comm is not None:
173 175 Widget.widgets.pop(self.model_id, None)
174 176 self.comm.close()
175 177 self.comm = None
176 178
177 179 def send_state(self, key=None):
178 180 """Sends the widget state, or a piece of it, to the front-end.
179 181
180 182 Parameters
181 183 ----------
182 184 key : unicode, or iterable (optional)
183 185 A single property's name or iterable of property names to sync with the front-end.
184 186 """
185 187 self._send({
186 188 "method" : "update",
187 189 "state" : self.get_state(key=key)
188 190 })
189 191
190 192 def get_state(self, key=None):
191 193 """Gets the widget state, or a piece of it.
192 194
193 195 Parameters
194 196 ----------
195 197 key : unicode or iterable (optional)
196 198 A single property's name or iterable of property names to get.
197 199 """
198 200 if key is None:
199 201 keys = self.keys
200 202 elif isinstance(key, string_types):
201 203 keys = [key]
202 204 elif isinstance(key, collections.Iterable):
203 205 keys = key
204 206 else:
205 207 raise ValueError("key must be a string, an iterable of keys, or None")
206 208 state = {}
207 209 for k in keys:
208 210 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
209 211 value = getattr(self, k)
210 212 state[k] = f(value)
211 213 return state
212 214
213 215 def set_state(self, sync_data):
214 216 """Called when a state is received from the front-end."""
215 217 for name in self.keys:
216 218 if name in sync_data:
217 219 json_value = sync_data[name]
218 220 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
219 221 with self._lock_property(name, json_value):
220 222 setattr(self, name, from_json(json_value))
221 223
222 224 def send(self, content):
223 225 """Sends a custom msg to the widget model in the front-end.
224 226
225 227 Parameters
226 228 ----------
227 229 content : dict
228 230 Content of the message to send.
229 231 """
230 232 self._send({"method": "custom", "content": content})
231 233
232 234 def on_msg(self, callback, remove=False):
233 235 """(Un)Register a custom msg receive callback.
234 236
235 237 Parameters
236 238 ----------
237 239 callback: callable
238 240 callback will be passed two arguments when a message arrives::
239 241
240 242 callback(widget, content)
241 243
242 244 remove: bool
243 245 True if the callback should be unregistered."""
244 246 self._msg_callbacks.register_callback(callback, remove=remove)
245 247
246 248 def on_displayed(self, callback, remove=False):
247 249 """(Un)Register a widget displayed callback.
248 250
249 251 Parameters
250 252 ----------
251 253 callback: method handler
252 254 Must have a signature of::
253 255
254 256 callback(widget, **kwargs)
255 257
256 258 kwargs from display are passed through without modification.
257 259 remove: bool
258 260 True if the callback should be unregistered."""
259 261 self._display_callbacks.register_callback(callback, remove=remove)
260 262
261 263 #-------------------------------------------------------------------------
262 264 # Support methods
263 265 #-------------------------------------------------------------------------
264 266 @contextmanager
265 267 def _lock_property(self, key, value):
266 268 """Lock a property-value pair.
267 269
268 270 The value should be the JSON state of the property.
269 271
270 272 NOTE: This, in addition to the single lock for all state changes, is
271 273 flawed. In the future we may want to look into buffering state changes
272 274 back to the front-end."""
273 275 self._property_lock = (key, value)
274 276 try:
275 277 yield
276 278 finally:
277 279 self._property_lock = (None, None)
278 280
279 281 @contextmanager
280 282 def hold_sync(self):
281 283 """Hold syncing any state until the context manager is released"""
282 284 # We increment a value so that this can be nested. Syncing will happen when
283 285 # all levels have been released.
284 286 self._send_state_lock += 1
285 287 try:
286 288 yield
287 289 finally:
288 290 self._send_state_lock -=1
289 291 if self._send_state_lock == 0:
290 292 self.send_state(self._states_to_send)
291 293 self._states_to_send.clear()
292 294
293 295 def _should_send_property(self, key, value):
294 296 """Check the property lock (property_lock)"""
295 297 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
296 298 if (key == self._property_lock[0]
297 299 and to_json(value) == self._property_lock[1]):
298 300 return False
299 301 elif self._send_state_lock > 0:
300 302 self._states_to_send.add(key)
301 303 return False
302 304 else:
303 305 return True
304 306
305 307 # Event handlers
306 308 @_show_traceback
307 309 def _handle_msg(self, msg):
308 310 """Called when a msg is received from the front-end"""
309 311 data = msg['content']['data']
310 312 method = data['method']
311 313 if not method in ['backbone', 'custom']:
312 314 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
313 315
314 316 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
315 317 if method == 'backbone' and 'sync_data' in data:
316 318 sync_data = data['sync_data']
317 319 self.set_state(sync_data) # handles all methods
318 320
319 321 # Handle a custom msg from the front-end
320 322 elif method == 'custom':
321 323 if 'content' in data:
322 324 self._handle_custom_msg(data['content'])
323 325
324 326 def _handle_custom_msg(self, content):
325 327 """Called when a custom msg is received."""
326 328 self._msg_callbacks(self, content)
327 329
328 330 def _notify_trait(self, name, old_value, new_value):
329 331 """Called when a property has been changed."""
330 332 # Trigger default traitlet callback machinery. This allows any user
331 333 # registered validation to be processed prior to allowing the widget
332 334 # machinery to handle the state.
333 335 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
334 336
335 337 # Send the state after the user registered callbacks for trait changes
336 338 # have all fired (allows for user to validate values).
337 339 if self.comm is not None and name in self.keys:
338 340 # Make sure this isn't information that the front-end just sent us.
339 341 if self._should_send_property(name, new_value):
340 342 # Send new state to front-end
341 343 self.send_state(key=name)
342 344
343 345 def _handle_displayed(self, **kwargs):
344 346 """Called when a view has been displayed for this widget instance"""
345 347 self._display_callbacks(self, **kwargs)
346 348
347 349 def _trait_to_json(self, x):
348 350 """Convert a trait value to json
349 351
350 352 Traverse lists/tuples and dicts and serialize their values as well.
351 353 Replace any widgets with their model_id
352 354 """
353 355 if isinstance(x, dict):
354 356 return {k: self._trait_to_json(v) for k, v in x.items()}
355 357 elif isinstance(x, (list, tuple)):
356 358 return [self._trait_to_json(v) for v in x]
357 359 elif isinstance(x, Widget):
358 360 return "IPY_MODEL_" + x.model_id
359 361 else:
360 362 return x # Value must be JSON-able
361 363
362 364 def _trait_from_json(self, x):
363 365 """Convert json values to objects
364 366
365 367 Replace any strings representing valid model id values to Widget references.
366 368 """
367 369 if isinstance(x, dict):
368 370 return {k: self._trait_from_json(v) for k, v in x.items()}
369 371 elif isinstance(x, (list, tuple)):
370 372 return [self._trait_from_json(v) for v in x]
371 373 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
372 374 # we want to support having child widgets at any level in a hierarchy
373 375 # trusting that a widget UUID will not appear out in the wild
374 376 return Widget.widgets[x[10:]]
375 377 else:
376 378 return x
377 379
378 380 def _ipython_display_(self, **kwargs):
379 381 """Called when `IPython.display.display` is called on the widget."""
380 382 # Show view.
381 383 if self._view_name is not None:
382 384 self._send({"method": "display"})
383 385 self._handle_displayed(**kwargs)
384 386
385 387 def _send(self, msg):
386 388 """Sends a message to the model in the front-end."""
387 389 self.comm.send(msg)
388 390
389 391
390 392 class DOMWidget(Widget):
391 393 visible = Bool(True, help="Whether the widget is visible.", sync=True)
392 394 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
393 395 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
394 396
395 397 width = CUnicode(sync=True)
396 398 height = CUnicode(sync=True)
397 399 padding = CUnicode(sync=True)
398 400 margin = CUnicode(sync=True)
399 401
400 402 color = Unicode(sync=True)
401 403 background_color = Unicode(sync=True)
402 404 border_color = Unicode(sync=True)
403 405
404 406 border_width = CUnicode(sync=True)
405 407 border_radius = CUnicode(sync=True)
406 408 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
407 409 'none',
408 410 'hidden',
409 411 'dotted',
410 412 'dashed',
411 413 'solid',
412 414 'double',
413 415 'groove',
414 416 'ridge',
415 417 'inset',
416 418 'outset',
417 419 'initial',
418 420 'inherit', ''],
419 421 default_value='', sync=True)
420 422
421 423 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
422 424 'normal',
423 425 'italic',
424 426 'oblique',
425 427 'initial',
426 428 'inherit', ''],
427 429 default_value='', sync=True)
428 430 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
429 431 'normal',
430 432 'bold',
431 433 'bolder',
432 434 'lighter',
433 435 'initial',
434 436 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
435 437 default_value='', sync=True)
436 438 font_size = CUnicode(sync=True)
437 439 font_family = Unicode(sync=True)
438 440
439 441 def __init__(self, *pargs, **kwargs):
440 442 super(DOMWidget, self).__init__(*pargs, **kwargs)
441 443
442 444 def _validate_border(name, old, new):
443 445 if new is not None and new != '':
444 446 if name != 'border_width' and not self.border_width:
445 447 self.border_width = 1
446 448 if name != 'border_style' and self.border_style == '':
447 449 self.border_style = 'solid'
448 450 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
General Comments 0
You need to be logged in to leave comments. Login now