##// END OF EJS Templates
Merge pull request #5963 from jdfreder/viewids...
Min RK -
r17186:c38a6218 merge
parent child Browse files
Show More
@@ -1,213 +1,214 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // WidgetModel, WidgetView, and WidgetManager
10 10 //============================================================================
11 11 /**
12 12 * Base Widget classes
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule widget
16 16 */
17 17
18 18 (function () {
19 19 "use strict";
20 20
21 21 // Use require.js 'define' method so that require.js is intelligent enough to
22 22 // syncronously load everything within this file when it is being 'required'
23 23 // elsewhere.
24 24 define(["underscore",
25 25 "backbone",
26 26 ], function (_, Backbone) {
27 27
28 28 //--------------------------------------------------------------------
29 29 // WidgetManager class
30 30 //--------------------------------------------------------------------
31 31 var WidgetManager = function (comm_manager) {
32 32 // Public constructor
33 33 WidgetManager._managers.push(this);
34 34
35 35 // Attach a comm manager to the
36 36 this.comm_manager = comm_manager;
37 37 this._models = {}; /* Dictionary of model ids and model instances */
38 38
39 39 // Register already-registered widget model types with the comm manager.
40 40 var that = this;
41 41 _.each(WidgetManager._model_types, function(model_type, model_name) {
42 42 that.comm_manager.register_target(model_name, $.proxy(that._handle_comm_open, that));
43 43 });
44 44 };
45 45
46 46 //--------------------------------------------------------------------
47 47 // Class level
48 48 //--------------------------------------------------------------------
49 49 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
50 50 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
51 51 WidgetManager._managers = []; /* List of widget managers */
52 52
53 53 WidgetManager.register_widget_model = function (model_name, model_type) {
54 54 // Registers a widget model by name.
55 55 WidgetManager._model_types[model_name] = model_type;
56 56
57 57 // Register the widget with the comm manager. Make sure to pass this object's context
58 58 // in so `this` works in the call back.
59 59 _.each(WidgetManager._managers, function(instance, i) {
60 60 if (instance.comm_manager !== null) {
61 61 instance.comm_manager.register_target(model_name, $.proxy(instance._handle_comm_open, instance));
62 62 }
63 63 });
64 64 };
65 65
66 66 WidgetManager.register_widget_view = function (view_name, view_type) {
67 67 // Registers a widget view by name.
68 68 WidgetManager._view_types[view_name] = view_type;
69 69 };
70 70
71 71 //--------------------------------------------------------------------
72 72 // Instance level
73 73 //--------------------------------------------------------------------
74 74 WidgetManager.prototype.display_view = function(msg, model) {
75 75 // Displays a view for a particular model.
76 76 var cell = this.get_msg_cell(msg.parent_header.msg_id);
77 77 if (cell === null) {
78 78 console.log("Could not determine where the display" +
79 79 " message was from. Widget will not be displayed");
80 80 } else {
81 81 var view = this.create_view(model, {cell: cell});
82 82 if (view === null) {
83 83 console.error("View creation failed", model);
84 84 }
85 85 if (cell.widget_subarea) {
86 86 cell.widget_area.show();
87 87 this._handle_display_view(view);
88 88 cell.widget_subarea.append(view.$el);
89 view.trigger('displayed');
89 90 }
90 91 }
91 92 };
92 93
93 94 WidgetManager.prototype._handle_display_view = function (view) {
94 95 // Have the IPython keyboard manager disable its event
95 96 // handling so the widget can capture keyboard input.
96 97 // Note, this is only done on the outer most widgets.
97 98 IPython.keyboard_manager.register_events(view.$el);
98 99
99 100 if (view.additional_elements) {
100 101 for (var i = 0; i < view.additional_elements.length; i++) {
101 102 IPython.keyboard_manager.register_events(view.additional_elements[i]);
102 103 }
103 104 }
104 105 };
105 106
106 107 WidgetManager.prototype.create_view = function(model, options, view) {
107 108 // Creates a view for a particular model.
108 109 var view_name = model.get('_view_name');
109 110 var ViewType = WidgetManager._view_types[view_name];
110 111 if (ViewType) {
111 112
112 113 // If a view is passed into the method, use that view's cell as
113 114 // the cell for the view that is created.
114 115 options = options || {};
115 116 if (view !== undefined) {
116 117 options.cell = view.options.cell;
117 118 }
118 119
119 120 // Create and render the view...
120 121 var parameters = {model: model, options: options};
121 122 view = new ViewType(parameters);
122 123 view.render();
123 124 model.on('destroy', view.remove, view);
124 125 return view;
125 126 }
126 127 return null;
127 128 };
128 129
129 130 WidgetManager.prototype.get_msg_cell = function (msg_id) {
130 131 var cell = null;
131 132 // First, check to see if the msg was triggered by cell execution.
132 133 if (IPython.notebook) {
133 134 cell = IPython.notebook.get_msg_cell(msg_id);
134 135 }
135 136 if (cell !== null) {
136 137 return cell;
137 138 }
138 139 // Second, check to see if a get_cell callback was defined
139 140 // for the message. get_cell callbacks are registered for
140 141 // widget messages, so this block is actually checking to see if the
141 142 // message was triggered by a widget.
142 143 var kernel = this.comm_manager.kernel;
143 144 if (kernel) {
144 145 var callbacks = kernel.get_callbacks_for_msg(msg_id);
145 146 if (callbacks && callbacks.iopub &&
146 147 callbacks.iopub.get_cell !== undefined) {
147 148 return callbacks.iopub.get_cell();
148 149 }
149 150 }
150 151
151 152 // Not triggered by a cell or widget (no get_cell callback
152 153 // exists).
153 154 return null;
154 155 };
155 156
156 157 WidgetManager.prototype.callbacks = function (view) {
157 158 // callback handlers specific a view
158 159 var callbacks = {};
159 160 if (view && view.options.cell) {
160 161
161 162 // Try to get output handlers
162 163 var cell = view.options.cell;
163 164 var handle_output = null;
164 165 var handle_clear_output = null;
165 166 if (cell.output_area) {
166 167 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
167 168 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
168 169 }
169 170
170 171 // Create callback dict using what is known
171 172 var that = this;
172 173 callbacks = {
173 174 iopub : {
174 175 output : handle_output,
175 176 clear_output : handle_clear_output,
176 177
177 178 // Special function only registered by widget messages.
178 179 // Allows us to get the cell for a message so we know
179 180 // where to add widgets if the code requires it.
180 181 get_cell : function () {
181 182 return cell;
182 183 },
183 184 },
184 185 };
185 186 }
186 187 return callbacks;
187 188 };
188 189
189 190 WidgetManager.prototype.get_model = function (model_id) {
190 191 // Look-up a model instance by its id.
191 192 var model = this._models[model_id];
192 193 if (model !== undefined && model.id == model_id) {
193 194 return model;
194 195 }
195 196 return null;
196 197 };
197 198
198 199 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
199 200 // Handle when a comm is opened.
200 201 var that = this;
201 202 var model_id = comm.comm_id;
202 203 var widget_type_name = msg.content.target_name;
203 204 var widget_model = new WidgetManager._model_types[widget_type_name](this, model_id, comm);
204 205 widget_model.on('comm:close', function () {
205 206 delete that._models[model_id];
206 207 });
207 208 this._models[model_id] = widget_model;
208 209 };
209 210
210 211 IPython.WidgetManager = WidgetManager;
211 212 return IPython.WidgetManager;
212 213 });
213 214 }());
@@ -1,452 +1,480 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Base Widget Model and View classes
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["widgets/js/manager",
18 18 "underscore",
19 19 "backbone"],
20 20 function(WidgetManager, _, Backbone){
21 21
22 22 var WidgetModel = Backbone.Model.extend({
23 23 constructor: function (widget_manager, model_id, comm) {
24 24 // Constructor
25 25 //
26 26 // Creates a WidgetModel instance.
27 27 //
28 28 // Parameters
29 29 // ----------
30 30 // widget_manager : WidgetManager instance
31 31 // model_id : string
32 32 // An ID unique to this model.
33 33 // comm : Comm instance (optional)
34 34 this.widget_manager = widget_manager;
35 35 this._buffered_state_diff = {};
36 36 this.pending_msgs = 0;
37 37 this.msg_buffer = null;
38 38 this.key_value_lock = null;
39 39 this.id = model_id;
40 40 this.views = [];
41 41
42 42 if (comm !== undefined) {
43 43 // Remember comm associated with the model.
44 44 this.comm = comm;
45 45 comm.model = this;
46 46
47 47 // Hook comm messages up to model.
48 48 comm.on_close($.proxy(this._handle_comm_closed, this));
49 49 comm.on_msg($.proxy(this._handle_comm_msg, this));
50 50 }
51 51 return Backbone.Model.apply(this);
52 52 },
53 53
54 54 send: function (content, callbacks) {
55 55 // Send a custom msg over the comm.
56 56 if (this.comm !== undefined) {
57 57 var data = {method: 'custom', content: content};
58 58 this.comm.send(data, callbacks);
59 59 this.pending_msgs++;
60 60 }
61 61 },
62 62
63 63 _handle_comm_closed: function (msg) {
64 64 // Handle when a widget is closed.
65 65 this.trigger('comm:close');
66 66 delete this.comm.model; // Delete ref so GC will collect widget model.
67 67 delete this.comm;
68 68 delete this.model_id; // Delete id from model so widget manager cleans up.
69 69 _.each(this.views, function(view, i) {
70 70 view.remove();
71 71 });
72 72 },
73 73
74 74 _handle_comm_msg: function (msg) {
75 75 // Handle incoming comm msg.
76 76 var method = msg.content.data.method;
77 77 switch (method) {
78 78 case 'update':
79 79 this.apply_update(msg.content.data.state);
80 80 break;
81 81 case 'custom':
82 82 this.trigger('msg:custom', msg.content.data.content);
83 83 break;
84 84 case 'display':
85 85 this.widget_manager.display_view(msg, this);
86 this.trigger('displayed');
87 86 break;
88 87 }
89 88 },
90 89
91 90 apply_update: function (state) {
92 91 // Handle when a widget is updated via the python side.
93 92 var that = this;
94 93 _.each(state, function(value, key) {
95 94 that.key_value_lock = [key, value];
96 95 try {
97 96 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
98 97 } finally {
99 98 that.key_value_lock = null;
100 99 }
101 100 });
102 101 },
103 102
104 103 _handle_status: function (msg, callbacks) {
105 104 // Handle status msgs.
106 105
107 106 // execution_state : ('busy', 'idle', 'starting')
108 107 if (this.comm !== undefined) {
109 108 if (msg.content.execution_state ==='idle') {
110 109 // Send buffer if this message caused another message to be
111 110 // throttled.
112 111 if (this.msg_buffer !== null &&
113 112 (this.get('msg_throttle') || 3) === this.pending_msgs) {
114 113 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
115 114 this.comm.send(data, callbacks);
116 115 this.msg_buffer = null;
117 116 } else {
118 117 --this.pending_msgs;
119 118 }
120 119 }
121 120 }
122 121 },
123 122
124 123 callbacks: function(view) {
125 124 // Create msg callbacks for a comm msg.
126 125 var callbacks = this.widget_manager.callbacks(view);
127 126
128 127 if (callbacks.iopub === undefined) {
129 128 callbacks.iopub = {};
130 129 }
131 130
132 131 var that = this;
133 132 callbacks.iopub.status = function (msg) {
134 133 that._handle_status(msg, callbacks);
135 134 };
136 135 return callbacks;
137 136 },
138 137
139 138 set: function(key, val, options) {
140 139 // Set a value.
141 140 var return_value = WidgetModel.__super__.set.apply(this, arguments);
142 141
143 142 // Backbone only remembers the diff of the most recent set()
144 143 // operation. Calling set multiple times in a row results in a
145 144 // loss of diff information. Here we keep our own running diff.
146 145 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
147 146 return return_value;
148 147 },
149 148
150 149 sync: function (method, model, options) {
151 150 // Handle sync to the back-end. Called when a model.save() is called.
152 151
153 152 // Make sure a comm exists.
154 153 var error = options.error || function() {
155 154 console.error('Backbone sync error:', arguments);
156 155 };
157 156 if (this.comm === undefined) {
158 157 error();
159 158 return false;
160 159 }
161 160
162 161 // Delete any key value pairs that the back-end already knows about.
163 162 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
164 163 if (this.key_value_lock !== null) {
165 164 var key = this.key_value_lock[0];
166 165 var value = this.key_value_lock[1];
167 166 if (attrs[key] === value) {
168 167 delete attrs[key];
169 168 }
170 169 }
171 170
172 171 // Only sync if there are attributes to send to the back-end.
173 172 attrs = this._pack_models(attrs);
174 173 if (_.size(attrs) > 0) {
175 174
176 175 // If this message was sent via backbone itself, it will not
177 176 // have any callbacks. It's important that we create callbacks
178 177 // so we can listen for status messages, etc...
179 178 var callbacks = options.callbacks || this.callbacks();
180 179
181 180 // Check throttle.
182 181 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
183 182 // The throttle has been exceeded, buffer the current msg so
184 183 // it can be sent once the kernel has finished processing
185 184 // some of the existing messages.
186 185
187 186 // Combine updates if it is a 'patch' sync, otherwise replace updates
188 187 switch (method) {
189 188 case 'patch':
190 189 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
191 190 break;
192 191 case 'update':
193 192 case 'create':
194 193 this.msg_buffer = attrs;
195 194 break;
196 195 default:
197 196 error();
198 197 return false;
199 198 }
200 199 this.msg_buffer_callbacks = callbacks;
201 200
202 201 } else {
203 202 // We haven't exceeded the throttle, send the message like
204 203 // normal.
205 204 var data = {method: 'backbone', sync_data: attrs};
206 205 this.comm.send(data, callbacks);
207 206 this.pending_msgs++;
208 207 }
209 208 }
210 209 // Since the comm is a one-way communication, assume the message
211 210 // arrived. Don't call success since we don't have a model back from the server
212 211 // this means we miss out on the 'sync' event.
213 212 this._buffered_state_diff = {};
214 213 },
215 214
216 215 save_changes: function(callbacks) {
217 216 // Push this model's state to the back-end
218 217 //
219 218 // This invokes a Backbone.Sync.
220 219 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
221 220 },
222 221
223 222 _pack_models: function(value) {
224 223 // Replace models with model ids recursively.
225 224 if (value instanceof Backbone.Model) {
226 225 return value.id;
227 226
228 227 } else if ($.isArray(value)) {
229 228 var packed = [];
230 229 var that = this;
231 230 _.each(value, function(sub_value, key) {
232 231 packed.push(that._pack_models(sub_value));
233 232 });
234 233 return packed;
235 234
236 235 } else if (value instanceof Object) {
237 236 var packed = {};
238 237 var that = this;
239 238 _.each(value, function(sub_value, key) {
240 239 packed[key] = that._pack_models(sub_value);
241 240 });
242 241 return packed;
243 242
244 243 } else {
245 244 return value;
246 245 }
247 246 },
248 247
249 248 _unpack_models: function(value) {
250 249 // Replace model ids with models recursively.
251 250 if ($.isArray(value)) {
252 251 var unpacked = [];
253 252 var that = this;
254 253 _.each(value, function(sub_value, key) {
255 254 unpacked.push(that._unpack_models(sub_value));
256 255 });
257 256 return unpacked;
258 257
259 258 } else if (value instanceof Object) {
260 259 var unpacked = {};
261 260 var that = this;
262 261 _.each(value, function(sub_value, key) {
263 262 unpacked[key] = that._unpack_models(sub_value);
264 263 });
265 264 return unpacked;
266 265
267 266 } else {
268 267 var model = this.widget_manager.get_model(value);
269 268 if (model) {
270 269 return model;
271 270 } else {
272 271 return value;
273 272 }
274 273 }
275 274 },
276 275
277 276 });
278 277 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
279 278
280 279
281 280 var WidgetView = Backbone.View.extend({
282 281 initialize: function(parameters) {
283 282 // Public constructor.
284 283 this.model.on('change',this.update,this);
285 284 this.options = parameters.options;
286 this.child_views = [];
285 this.child_model_views = {};
286 this.child_views = {};
287 287 this.model.views.push(this);
288 this.id = this.id || IPython.utils.uuid();
288 289 },
289 290
290 291 update: function(){
291 292 // Triggered on model change.
292 293 //
293 294 // Update view to be consistent with this.model
294 295 },
295 296
296 297 create_child_view: function(child_model, options) {
297 298 // Create and return a child view.
298 299 //
299 300 // -given a model and (optionally) a view name if the view name is
300 301 // not given, it defaults to the model's default view attribute.
301 302
302 303 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
303 304 // it would be great to have the widget manager add the cell metadata
304 305 // to the subview without having to add it here.
305 var child_view = this.model.widget_manager.create_view(child_model, options || {}, this);
306 this.child_views[child_model.id] = child_view;
306 options = $.extend({ parent: this }, options || {});
307 var child_view = this.model.widget_manager.create_view(child_model, options, this);
308
309 // Associate the view id with the model id.
310 if (this.child_model_views[child_model.id] === undefined) {
311 this.child_model_views[child_model.id] = [];
312 }
313 this.child_model_views[child_model.id].push(child_view.id);
314
315 // Remember the view by id.
316 this.child_views[child_view.id] = child_view;
307 317 return child_view;
308 318 },
309 319
310 delete_child_view: function(child_model, options) {
320 pop_child_view: function(child_model) {
311 321 // Delete a child view that was previously created using create_child_view.
312 var view = this.child_views[child_model.id];
313 if (view !== undefined) {
314 delete this.child_views[child_model.id];
315 view.remove();
322 var view_ids = this.child_model_views[child_model.id];
323 if (view_ids !== undefined) {
324
325 // Only delete the first view in the list.
326 var view_id = view_ids[0];
327 var view = this.child_views[view_id];
328 delete this.child_views[view_id];
329 view_ids.splice(0,1);
316 330 child_model.views.pop(view);
331
332 // Remove the view list specific to this model if it is empty.
333 if (view_ids.length === 0) {
334 delete this.child_model_views[child_model.id];
335 }
336 return view;
317 337 }
338 return null;
318 339 },
319 340
320 341 do_diff: function(old_list, new_list, removed_callback, added_callback) {
321 342 // Difference a changed list and call remove and add callbacks for
322 343 // each removed and added item in the new list.
323 344 //
324 345 // Parameters
325 346 // ----------
326 347 // old_list : array
327 348 // new_list : array
328 349 // removed_callback : Callback(item)
329 350 // Callback that is called for each item removed.
330 351 // added_callback : Callback(item)
331 352 // Callback that is called for each item added.
332 353
354 // Walk the lists until an unequal entry is found.
355 var i;
356 for (i = 0; i < new_list.length; i++) {
357 if (i < old_list.length || new_list[i] !== old_list[i]) {
358 break;
359 }
360 }
333 361
334 // removed items
335 _.each(_.difference(old_list, new_list), function(item, index, list) {
336 removed_callback(item);
337 }, this);
362 // Remove the non-matching items from the old list.
363 for (var j = i; j < old_list.length; j++) {
364 removed_callback(old_list[j]);
365 }
338 366
339 // added items
340 _.each(_.difference(new_list, old_list), function(item, index, list) {
341 added_callback(item);
342 }, this);
367 // Add the rest of the new list items.
368 for (i; i < new_list.length; i++) {
369 added_callback(new_list[i]);
370 }
343 371 },
344 372
345 373 callbacks: function(){
346 374 // Create msg callbacks for a comm msg.
347 375 return this.model.callbacks(this);
348 376 },
349 377
350 378 render: function(){
351 379 // Render the view.
352 380 //
353 381 // By default, this is only called the first time the view is created
354 382 },
355 383
356 384 send: function (content) {
357 385 // Send a custom msg associated with this view.
358 386 this.model.send(content, this.callbacks());
359 387 },
360 388
361 389 touch: function () {
362 390 this.model.save_changes(this.callbacks());
363 391 },
364 392 });
365 393
366 394
367 395 var DOMWidgetView = WidgetView.extend({
368 396 initialize: function (options) {
369 397 // Public constructor
370 398
371 399 // In the future we may want to make changes more granular
372 400 // (e.g., trigger on visible:change).
373 401 this.model.on('change', this.update, this);
374 402 this.model.on('msg:custom', this.on_msg, this);
375 403 DOMWidgetView.__super__.initialize.apply(this, arguments);
376 404 },
377 405
378 406 on_msg: function(msg) {
379 407 // Handle DOM specific msgs.
380 408 switch(msg.msg_type) {
381 409 case 'add_class':
382 410 this.add_class(msg.selector, msg.class_list);
383 411 break;
384 412 case 'remove_class':
385 413 this.remove_class(msg.selector, msg.class_list);
386 414 break;
387 415 }
388 416 },
389 417
390 418 add_class: function (selector, class_list) {
391 419 // Add a DOM class to an element.
392 420 this._get_selector_element(selector).addClass(class_list);
393 421 },
394 422
395 423 remove_class: function (selector, class_list) {
396 424 // Remove a DOM class from an element.
397 425 this._get_selector_element(selector).removeClass(class_list);
398 426 },
399 427
400 428 update: function () {
401 429 // Update the contents of this view
402 430 //
403 431 // Called when the model is changed. The model may have been
404 432 // changed by another view or by a state update from the back-end.
405 433 // The very first update seems to happen before the element is
406 434 // finished rendering so we use setTimeout to give the element time
407 435 // to render
408 436 var e = this.$el;
409 437 var visible = this.model.get('visible');
410 438 setTimeout(function() {e.toggle(visible);},0);
411 439
412 440 var css = this.model.get('_css');
413 441 if (css === undefined) {return;}
414 442 for (var i = 0; i < css.length; i++) {
415 443 // Apply the css traits to all elements that match the selector.
416 444 var selector = css[i][0];
417 445 var elements = this._get_selector_element(selector);
418 446 if (elements.length > 0) {
419 447 var trait_key = css[i][1];
420 448 var trait_value = css[i][2];
421 449 elements.css(trait_key ,trait_value);
422 450 }
423 451 }
424 452 },
425 453
426 454 _get_selector_element: function (selector) {
427 455 // Get the elements via the css selector.
428 456
429 457 // If the selector is blank, apply the style to the $el_to_style
430 458 // element. If the $el_to_style element is not defined, use apply
431 459 // the style to the view's element.
432 460 var elements;
433 461 if (!selector) {
434 462 if (this.$el_to_style === undefined) {
435 463 elements = this.$el;
436 464 } else {
437 465 elements = this.$el_to_style;
438 466 }
439 467 } else {
440 468 elements = this.$el.find(selector);
441 469 }
442 470 return elements;
443 471 },
444 472 });
445 473
446 474 IPython.WidgetModel = WidgetModel;
447 475 IPython.WidgetView = WidgetView;
448 476 IPython.DOMWidgetView = DOMWidgetView;
449 477
450 478 // Pass through WidgetManager namespace.
451 479 return WidgetManager;
452 480 });
@@ -1,325 +1,321 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // ContainerWidget
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["widgets/js/widget"], function(WidgetManager) {
18 18
19 19 var ContainerView = IPython.DOMWidgetView.extend({
20 20 render: function(){
21 21 // Called when view is rendered.
22 22 this.$el.addClass('widget-container')
23 23 .addClass('vbox');
24 this.children={};
25 this.update_children([], this.model.get('_children'));
26 this.model.on('change:_children', function(model, value, options) {
27 this.update_children(model.previous('_children'), value);
24 this.update_children([], this.model.get('children'));
25 this.model.on('change:children', function(model, value, options) {
26 this.update_children(model.previous('children'), value);
28 27 }, this);
29 28 this.update();
30 29
31 30 // Trigger model displayed events for any models that are child to
32 31 // this model when this model is displayed.
33 32 var that = this;
34 this.model.on('displayed', function(){
33 this.on('displayed', function(){
35 34 that.is_displayed = true;
36 35 for (var property in that.child_views) {
37 36 if (that.child_views.hasOwnProperty(property)) {
38 that.child_views[property].model.trigger('displayed');
37 that.child_views[property].trigger('displayed');
39 38 }
40 39 }
41 40 });
42 41 },
43 42
44 43 update_children: function(old_list, new_list) {
45 44 // Called when the children list changes.
46 45 this.do_diff(old_list,
47 46 new_list,
48 47 $.proxy(this.remove_child_model, this),
49 48 $.proxy(this.add_child_model, this));
50 49 },
51 50
52 51 remove_child_model: function(model) {
53 52 // Called when a model is removed from the children list.
54 this.child_views[model.id].remove();
55 this.delete_child_view(model);
53 this.pop_child_view(model).remove();
56 54 },
57 55
58 56 add_child_model: function(model) {
59 57 // Called when a model is added to the children list.
60 58 var view = this.create_child_view(model);
61 59 this.$el.append(view.$el);
62 60
63 61 // Trigger the displayed event if this model is displayed.
64 62 if (this.is_displayed) {
65 model.trigger('displayed');
63 view.trigger('displayed');
66 64 }
67 65 },
68 66
69 67 update: function(){
70 68 // Update the contents of this view
71 69 //
72 70 // Called when the model is changed. The model may have been
73 71 // changed by another view or by a state update from the back-end.
74 72 return ContainerView.__super__.update.apply(this);
75 73 },
76 74 });
77 75
78 76 WidgetManager.register_widget_view('ContainerView', ContainerView);
79 77
80 78 var PopupView = IPython.DOMWidgetView.extend({
81 79 render: function(){
82 80 // Called when view is rendered.
83 81 var that = this;
84 this.children={};
85 82
86 83 this.$el.on("remove", function(){
87 84 that.$backdrop.remove();
88 85 });
89 86 this.$backdrop = $('<div />')
90 87 .appendTo($('#notebook-container'))
91 88 .addClass('modal-dialog')
92 89 .css('position', 'absolute')
93 90 .css('left', '0px')
94 91 .css('top', '0px');
95 92 this.$window = $('<div />')
96 93 .appendTo(this.$backdrop)
97 94 .addClass('modal-content widget-modal')
98 95 .mousedown(function(){
99 96 that.bring_to_front();
100 97 });
101 98
102 99 // Set the elements array since the this.$window element is not child
103 100 // of this.$el and the parent widget manager or other widgets may
104 101 // need to know about all of the top-level widgets. The IPython
105 102 // widget manager uses this to register the elements with the
106 103 // keyboard manager.
107 104 this.additional_elements = [this.$window];
108 105
109 106 this.$title_bar = $('<div />')
110 107 .addClass('popover-title')
111 108 .appendTo(this.$window)
112 109 .mousedown(function(){
113 110 that.bring_to_front();
114 111 });
115 112 this.$close = $('<button />')
116 113 .addClass('close icon-remove')
117 114 .css('margin-left', '5px')
118 115 .appendTo(this.$title_bar)
119 116 .click(function(){
120 117 that.hide();
121 118 event.stopPropagation();
122 119 });
123 120 this.$minimize = $('<button />')
124 121 .addClass('close icon-arrow-down')
125 122 .appendTo(this.$title_bar)
126 123 .click(function(){
127 124 that.popped_out = !that.popped_out;
128 125 if (!that.popped_out) {
129 126 that.$minimize
130 127 .removeClass('icon-arrow-down')
131 128 .addClass('icon-arrow-up');
132 129
133 130 that.$window
134 131 .draggable('destroy')
135 132 .resizable('destroy')
136 133 .removeClass('widget-modal modal-content')
137 134 .addClass('docked-widget-modal')
138 135 .detach()
139 136 .insertBefore(that.$show_button);
140 137 that.$show_button.hide();
141 138 that.$close.hide();
142 139 } else {
143 140 that.$minimize
144 141 .addClass('icon-arrow-down')
145 142 .removeClass('icon-arrow-up');
146 143
147 144 that.$window
148 145 .removeClass('docked-widget-modal')
149 146 .addClass('widget-modal modal-content')
150 147 .detach()
151 148 .appendTo(that.$backdrop)
152 149 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
153 150 .resizable()
154 151 .children('.ui-resizable-handle').show();
155 152 that.show();
156 153 that.$show_button.show();
157 154 that.$close.show();
158 155 }
159 156 event.stopPropagation();
160 157 });
161 158 this.$title = $('<div />')
162 159 .addClass('widget-modal-title')
163 160 .html("&nbsp;")
164 161 .appendTo(this.$title_bar);
165 162 this.$body = $('<div />')
166 163 .addClass('modal-body')
167 164 .addClass('widget-modal-body')
168 165 .addClass('widget-container')
169 166 .addClass('vbox')
170 167 .appendTo(this.$window);
171 168
172 169 this.$show_button = $('<button />')
173 170 .html("&nbsp;")
174 171 .addClass('btn btn-info widget-modal-show')
175 172 .appendTo(this.$el)
176 173 .click(function(){
177 174 that.show();
178 175 });
179 176
180 177 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
181 178 this.$window.resizable();
182 179 this.$window.on('resize', function(){
183 180 that.$body.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
184 181 });
185 182
186 183 this.$el_to_style = this.$body;
187 184 this._shown_once = false;
188 185 this.popped_out = true;
189 186
190 this.update_children([], this.model.get('_children'));
191 this.model.on('change:_children', function(model, value, options) {
192 this.update_children(model.previous('_children'), value);
187 this.update_children([], this.model.get('children'));
188 this.model.on('change:children', function(model, value, options) {
189 this.update_children(model.previous('children'), value);
193 190 }, this);
194 191 this.update();
195 192
196 193 // Trigger model displayed events for any models that are child to
197 194 // this model when this model is displayed.
198 this.model.on('displayed', function(){
195 this.on('displayed', function(){
199 196 that.is_displayed = true;
200 197 for (var property in that.child_views) {
201 198 if (that.child_views.hasOwnProperty(property)) {
202 that.child_views[property].model.trigger('displayed');
199 that.child_views[property].trigger('displayed');
203 200 }
204 201 }
205 202 });
206 203 },
207 204
208 205 hide: function() {
209 206 // Called when the modal hide button is clicked.
210 207 this.$window.hide();
211 208 this.$show_button.removeClass('btn-info');
212 209 },
213 210
214 211 show: function() {
215 212 // Called when the modal show button is clicked.
216 213 this.$show_button.addClass('btn-info');
217 214 this.$window.show();
218 215 if (this.popped_out) {
219 216 this.$window.css("positon", "absolute");
220 217 this.$window.css("top", "0px");
221 218 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
222 219 $(window).scrollLeft()) + "px");
223 220 this.bring_to_front();
224 221 }
225 222 },
226 223
227 224 bring_to_front: function() {
228 225 // Make the modal top-most, z-ordered about the other modals.
229 226 var $widget_modals = $(".widget-modal");
230 227 var max_zindex = 0;
231 228 $widget_modals.each(function (index, el){
232 229 var zindex = parseInt($(el).css('z-index'));
233 230 if (!isNaN(zindex)) {
234 231 max_zindex = Math.max(max_zindex, zindex);
235 232 }
236 233 });
237 234
238 235 // Start z-index of widget modals at 2000
239 236 max_zindex = Math.max(max_zindex, 2000);
240 237
241 238 $widget_modals.each(function (index, el){
242 239 $el = $(el);
243 240 if (max_zindex == parseInt($el.css('z-index'))) {
244 241 $el.css('z-index', max_zindex - 1);
245 242 }
246 243 });
247 244 this.$window.css('z-index', max_zindex);
248 245 },
249 246
250 247 update_children: function(old_list, new_list) {
251 248 // Called when the children list is modified.
252 249 this.do_diff(old_list,
253 250 new_list,
254 251 $.proxy(this.remove_child_model, this),
255 252 $.proxy(this.add_child_model, this));
256 253 },
257 254
258 255 remove_child_model: function(model) {
259 256 // Called when a child is removed from children list.
260 this.child_views[model.id].remove();
261 this.delete_child_view(model);
257 this.pop_child_view(model).remove();
262 258 },
263 259
264 260 add_child_model: function(model) {
265 261 // Called when a child is added to children list.
266 262 var view = this.create_child_view(model);
267 263 this.$body.append(view.$el);
268 264
269 265 // Trigger the displayed event if this model is displayed.
270 266 if (this.is_displayed) {
271 model.trigger('displayed');
267 view.trigger('displayed');
272 268 }
273 269 },
274 270
275 271 update: function(){
276 272 // Update the contents of this view
277 273 //
278 274 // Called when the model is changed. The model may have been
279 275 // changed by another view or by a state update from the back-end.
280 276 var description = this.model.get('description');
281 277 if (description.trim().length === 0) {
282 278 this.$title.html("&nbsp;"); // Preserve title height
283 279 } else {
284 280 this.$title.text(description);
285 281 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
286 282 }
287 283
288 284 var button_text = this.model.get('button_text');
289 285 if (button_text.trim().length === 0) {
290 286 this.$show_button.html("&nbsp;"); // Preserve button height
291 287 } else {
292 288 this.$show_button.text(button_text);
293 289 }
294 290
295 291 if (!this._shown_once) {
296 292 this._shown_once = true;
297 293 this.show();
298 294 }
299 295
300 296 return PopupView.__super__.update.apply(this);
301 297 },
302 298
303 299 _get_selector_element: function(selector) {
304 300 // Get an element view a 'special' jquery selector. (see widget.js)
305 301 //
306 302 // Since the modal actually isn't within the $el in the DOM, we need to extend
307 303 // the selector logic to allow the user to set css on the modal if need be.
308 304 // The convention used is:
309 305 // "modal" - select the modal div
310 306 // "modal [selector]" - select element(s) within the modal div.
311 307 // "[selector]" - select elements within $el
312 308 // "" - select the $el_to_style
313 309 if (selector.substring(0, 5) == 'modal') {
314 310 if (selector == 'modal') {
315 311 return this.$window;
316 312 } else {
317 313 return this.$window.find(selector.substring(6));
318 314 }
319 315 } else {
320 316 return PopupView.__super__._get_selector_element.apply(this, [selector]);
321 317 }
322 318 },
323 319 });
324 320 WidgetManager.register_widget_view('PopupView', PopupView);
325 321 });
@@ -1,311 +1,312 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // IntWidget
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["widgets/js/widget"], function(WidgetManager){
18 18
19 19 var IntSliderView = IPython.DOMWidgetView.extend({
20 20 render : function(){
21 21 // Called when view is rendered.
22 22 this.$el
23 23 .addClass('widget-hbox-single');
24 24 this.$label = $('<div />')
25 25 .appendTo(this.$el)
26 26 .addClass('widget-hlabel')
27 27 .hide();
28 28
29 29 this.$slider = $('<div />')
30 30 .slider({})
31 31 .addClass('slider');
32 32 // Put the slider in a container
33 33 this.$slider_container = $('<div />')
34 34 .addClass('widget-hslider')
35 35 .append(this.$slider);
36 36 this.$el_to_style = this.$slider_container; // Set default element to style
37 37 this.$el.append(this.$slider_container);
38 38
39 39 this.$readout = $('<div/>')
40 40 .appendTo(this.$el)
41 41 .addClass('widget-hreadout')
42 42 .hide();
43 43
44 44 // Set defaults.
45 45 this.update();
46 46 },
47 47
48 48 update : function(options){
49 49 // Update the contents of this view
50 50 //
51 51 // Called when the model is changed. The model may have been
52 52 // changed by another view or by a state update from the back-end.
53 53 if (options === undefined || options.updated_view != this) {
54 54 // JQuery slider option keys. These keys happen to have a
55 55 // one-to-one mapping with the corrosponding keys of the model.
56 56 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
57 57 var that = this;
58 that.$slider.slider({});
58 59 _.each(jquery_slider_keys, function(key, i) {
59 60 var model_value = that.model.get(key);
60 61 if (model_value !== undefined) {
61 62 that.$slider.slider("option", key, model_value);
62 63 }
63 64 });
64 65
65 66 // WORKAROUND FOR JQUERY SLIDER BUG.
66 67 // The horizontal position of the slider handle
67 68 // depends on the value of the slider at the time
68 69 // of orientation change. Before applying the new
69 70 // workaround, we set the value to the minimum to
70 71 // make sure that the horizontal placement of the
71 72 // handle in the vertical slider is always
72 73 // consistent.
73 74 var orientation = this.model.get('orientation');
74 75 var value = this.model.get('min');
75 76 this.$slider.slider('option', 'value', value);
76 77 this.$slider.slider('option', 'orientation', orientation);
77 78 value = this.model.get('value');
78 79 this.$slider.slider('option', 'value', value);
79 80 this.$readout.text(value);
80 81
81 82 // Use the right CSS classes for vertical & horizontal sliders
82 83 if (orientation=='vertical') {
83 84 this.$slider_container
84 85 .removeClass('widget-hslider')
85 86 .addClass('widget-vslider');
86 87 this.$el
87 88 .removeClass('widget-hbox-single')
88 89 .addClass('widget-vbox-single');
89 90 this.$label
90 91 .removeClass('widget-hlabel')
91 92 .addClass('widget-vlabel');
92 93 this.$readout
93 94 .removeClass('widget-hreadout')
94 95 .addClass('widget-vreadout');
95 96
96 97 } else {
97 98 this.$slider_container
98 99 .removeClass('widget-vslider')
99 100 .addClass('widget-hslider');
100 101 this.$el
101 102 .removeClass('widget-vbox-single')
102 103 .addClass('widget-hbox-single');
103 104 this.$label
104 105 .removeClass('widget-vlabel')
105 106 .addClass('widget-hlabel');
106 107 this.$readout
107 108 .removeClass('widget-vreadout')
108 109 .addClass('widget-hreadout');
109 110 }
110 111
111 112 var description = this.model.get('description');
112 113 if (description.length === 0) {
113 114 this.$label.hide();
114 115 } else {
115 116 this.$label.text(description);
116 117 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
117 118 this.$label.show();
118 119 }
119 120
120 121 var readout = this.model.get('readout');
121 122 if (readout) {
122 123 this.$readout.show();
123 124 } else {
124 125 this.$readout.hide();
125 126 }
126 127 }
127 128 return IntSliderView.__super__.update.apply(this);
128 129 },
129 130
130 131 events: {
131 132 // Dictionary of events and their handlers.
132 133 "slide" : "handleSliderChange"
133 134 },
134 135
135 136 handleSliderChange: function(e, ui) {
136 137 // Called when the slider value is changed.
137 138
138 139 // Calling model.set will trigger all of the other views of the
139 140 // model to update.
140 141 var actual_value = this._validate_slide_value(ui.value);
141 142 this.model.set('value', actual_value, {updated_view: this});
142 143 this.$readout.text(actual_value);
143 144 this.touch();
144 145 },
145 146
146 147 _validate_slide_value: function(x) {
147 148 // Validate the value of the slider before sending it to the back-end
148 149 // and applying it to the other views on the page.
149 150
150 151 // Double bit-wise not truncates the decimel (int cast).
151 152 return ~~x;
152 153 },
153 154 });
154 155 WidgetManager.register_widget_view('IntSliderView', IntSliderView);
155 156
156 157
157 158 var IntTextView = IPython.DOMWidgetView.extend({
158 159 render : function(){
159 160 // Called when view is rendered.
160 161 this.$el
161 162 .addClass('widget-hbox-single');
162 163 this.$label = $('<div />')
163 164 .appendTo(this.$el)
164 165 .addClass('widget-hlabel')
165 166 .hide();
166 167 this.$textbox = $('<input type="text" />')
167 168 .addClass('form-control')
168 169 .addClass('widget-numeric-text')
169 170 .appendTo(this.$el);
170 171 this.$el_to_style = this.$textbox; // Set default element to style
171 172 this.update(); // Set defaults.
172 173 },
173 174
174 175 update : function(options){
175 176 // Update the contents of this view
176 177 //
177 178 // Called when the model is changed. The model may have been
178 179 // changed by another view or by a state update from the back-end.
179 180 if (options === undefined || options.updated_view != this) {
180 181 var value = this.model.get('value');
181 182 if (this._parse_value(this.$textbox.val()) != value) {
182 183 this.$textbox.val(value);
183 184 }
184 185
185 186 if (this.model.get('disabled')) {
186 187 this.$textbox.attr('disabled','disabled');
187 188 } else {
188 189 this.$textbox.removeAttr('disabled');
189 190 }
190 191
191 192 var description = this.model.get('description');
192 193 if (description.length === 0) {
193 194 this.$label.hide();
194 195 } else {
195 196 this.$label.text(description);
196 197 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
197 198 this.$label.show();
198 199 }
199 200 }
200 201 return IntTextView.__super__.update.apply(this);
201 202 },
202 203
203 204 events: {
204 205 // Dictionary of events and their handlers.
205 206 "keyup input" : "handleChanging",
206 207 "paste input" : "handleChanging",
207 208 "cut input" : "handleChanging",
208 209
209 210 // Fires only when control is validated or looses focus.
210 211 "change input" : "handleChanged"
211 212 },
212 213
213 214 handleChanging: function(e) {
214 215 // Handles and validates user input.
215 216
216 217 // Try to parse value as a int.
217 218 var numericalValue = 0;
218 219 if (e.target.value !== '') {
219 220 var trimmed = e.target.value.trim();
220 221 if (!(['-', '-.', '.', '+.', '+'].indexOf(trimmed) >= 0)) {
221 222 numericalValue = this._parse_value(e.target.value);
222 223 }
223 224 }
224 225
225 226 // If parse failed, reset value to value stored in model.
226 227 if (isNaN(numericalValue)) {
227 228 e.target.value = this.model.get('value');
228 229 } else if (!isNaN(numericalValue)) {
229 230 if (this.model.get('max') !== undefined) {
230 231 numericalValue = Math.min(this.model.get('max'), numericalValue);
231 232 }
232 233 if (this.model.get('min') !== undefined) {
233 234 numericalValue = Math.max(this.model.get('min'), numericalValue);
234 235 }
235 236
236 237 // Apply the value if it has changed.
237 238 if (numericalValue != this.model.get('value')) {
238 239
239 240 // Calling model.set will trigger all of the other views of the
240 241 // model to update.
241 242 this.model.set('value', numericalValue, {updated_view: this});
242 243 this.touch();
243 244 }
244 245 }
245 246 },
246 247
247 248 handleChanged: function(e) {
248 249 // Applies validated input.
249 250 if (this.model.get('value') != e.target.value) {
250 251 e.target.value = this.model.get('value');
251 252 }
252 253 },
253 254
254 255 _parse_value: function(value) {
255 256 // Parse the value stored in a string.
256 257 return parseInt(value);
257 258 },
258 259 });
259 260 WidgetManager.register_widget_view('IntTextView', IntTextView);
260 261
261 262
262 263 var ProgressView = IPython.DOMWidgetView.extend({
263 264 render : function(){
264 265 // Called when view is rendered.
265 266 this.$el
266 267 .addClass('widget-hbox-single');
267 268 this.$label = $('<div />')
268 269 .appendTo(this.$el)
269 270 .addClass('widget-hlabel')
270 271 .hide();
271 272 this.$progress = $('<div />')
272 273 .addClass('progress')
273 274 .addClass('widget-progress')
274 275 .appendTo(this.$el);
275 276 this.$el_to_style = this.$progress; // Set default element to style
276 277 this.$bar = $('<div />')
277 278 .addClass('progress-bar')
278 279 .css('width', '50%')
279 280 .appendTo(this.$progress);
280 281 this.update(); // Set defaults.
281 282 },
282 283
283 284 update : function(){
284 285 // Update the contents of this view
285 286 //
286 287 // Called when the model is changed. The model may have been
287 288 // changed by another view or by a state update from the back-end.
288 289 var value = this.model.get('value');
289 290 var max = this.model.get('max');
290 291 var min = this.model.get('min');
291 292 var percent = 100.0 * (value - min) / (max - min);
292 293 this.$bar.css('width', percent + '%');
293 294
294 295 var description = this.model.get('description');
295 296 if (description.length === 0) {
296 297 this.$label.hide();
297 298 } else {
298 299 this.$label.text(description);
299 300 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
300 301 this.$label.show();
301 302 }
302 303 return ProgressView.__super__.update.apply(this);
303 304 },
304 305 });
305 306 WidgetManager.register_widget_view('ProgressView', ProgressView);
306 307
307 308
308 309 // Return the slider and text views so they can be inheritted to create the
309 310 // float versions.
310 311 return [IntSliderView, IntTextView];
311 312 });
@@ -1,273 +1,272 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // SelectionContainerWidget
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["widgets/js/widget"], function(WidgetManager){
18 18
19 19 var AccordionView = IPython.DOMWidgetView.extend({
20 20 render: function(){
21 21 // Called when view is rendered.
22 22 var guid = 'panel-group' + IPython.utils.uuid();
23 23 this.$el
24 24 .attr('id', guid)
25 25 .addClass('panel-group');
26 26 this.containers = [];
27 27 this.model_containers = {};
28 this.update_children([], this.model.get('_children'));
29 this.model.on('change:_children', function(model, value, options) {
30 this.update_children(model.previous('_children'), value);
28 this.update_children([], this.model.get('children'));
29 this.model.on('change:children', function(model, value, options) {
30 this.update_children(model.previous('children'), value);
31 31 }, this);
32 32 this.model.on('change:selected_index', function(model, value, options) {
33 33 this.update_selected_index(model.previous('selected_index'), value, options);
34 34 }, this);
35 35 this.model.on('change:_titles', function(model, value, options) {
36 36 this.update_titles(value);
37 37 }, this);
38 38 var that = this;
39 this.model.on('displayed', function() {
39 this.on('displayed', function() {
40 40 this.update_titles();
41 41 // Trigger model displayed events for any models that are child to
42 42 // this model when this model is displayed.
43 43 that.is_displayed = true;
44 44 for (var property in that.child_views) {
45 45 if (that.child_views.hasOwnProperty(property)) {
46 that.child_views[property].model.trigger('displayed');
46 that.child_views[property].trigger('displayed');
47 47 }
48 48 }
49 49 }, this);
50 50 },
51 51
52 52 update_titles: function(titles) {
53 53 // Set tab titles
54 54 if (!titles) {
55 55 titles = this.model.get('_titles');
56 56 }
57 57
58 58 var that = this;
59 59 _.each(titles, function(title, page_index) {
60 60 var accordian = that.containers[page_index];
61 61 if (accordian !== undefined) {
62 62 accordian
63 63 .find('.panel-heading')
64 64 .find('.accordion-toggle')
65 65 .text(title);
66 66 }
67 67 });
68 68 },
69 69
70 70 update_selected_index: function(old_index, new_index, options) {
71 71 // Only update the selection if the selection wasn't triggered
72 72 // by the front-end. It must be triggered by the back-end.
73 73 if (options === undefined || options.updated_view != this) {
74 74 this.containers[old_index].find('.panel-collapse').collapse('hide');
75 75 if (0 <= new_index && new_index < this.containers.length) {
76 76 this.containers[new_index].find('.panel-collapse').collapse('show');
77 77 }
78 78 }
79 79 },
80 80
81 81 update_children: function(old_list, new_list) {
82 82 // Called when the children list is modified.
83 83 this.do_diff(old_list,
84 84 new_list,
85 85 $.proxy(this.remove_child_model, this),
86 86 $.proxy(this.add_child_model, this));
87 87 },
88 88
89 89 remove_child_model: function(model) {
90 90 // Called when a child is removed from children list.
91 91 var accordion_group = this.model_containers[model.id];
92 92 this.containers.splice(accordion_group.container_index, 1);
93 93 delete this.model_containers[model.id];
94 94 accordion_group.remove();
95 this.delete_child_view(model);
95 this.pop_child_view(model);
96 96 },
97 97
98 98 add_child_model: function(model) {
99 99 // Called when a child is added to children list.
100 100 var view = this.create_child_view(model);
101 101 var index = this.containers.length;
102 102 var uuid = IPython.utils.uuid();
103 103 var accordion_group = $('<div />')
104 104 .addClass('panel panel-default')
105 105 .appendTo(this.$el);
106 106 var accordion_heading = $('<div />')
107 107 .addClass('panel-heading')
108 108 .appendTo(accordion_group);
109 109 var that = this;
110 110 var accordion_toggle = $('<a />')
111 111 .addClass('accordion-toggle')
112 112 .attr('data-toggle', 'collapse')
113 113 .attr('data-parent', '#' + this.$el.attr('id'))
114 114 .attr('href', '#' + uuid)
115 115 .click(function(evt){
116 116
117 117 // Calling model.set will trigger all of the other views of the
118 118 // model to update.
119 119 that.model.set("selected_index", index, {updated_view: that});
120 120 that.touch();
121 121 })
122 122 .text('Page ' + index)
123 123 .appendTo(accordion_heading);
124 124 var accordion_body = $('<div />', {id: uuid})
125 125 .addClass('panel-collapse collapse')
126 126 .appendTo(accordion_group);
127 127 var accordion_inner = $('<div />')
128 128 .addClass('panel-body')
129 129 .appendTo(accordion_body);
130 130 var container_index = this.containers.push(accordion_group) - 1;
131 131 accordion_group.container_index = container_index;
132 132 this.model_containers[model.id] = accordion_group;
133 133 accordion_inner.append(view.$el);
134 134
135 135 this.update();
136 136 this.update_titles();
137 137
138 138 // Trigger the displayed event if this model is displayed.
139 139 if (this.is_displayed) {
140 model.trigger('displayed');
140 view.trigger('displayed');
141 141 }
142 142 },
143 143 });
144 144 WidgetManager.register_widget_view('AccordionView', AccordionView);
145 145
146 146
147 147 var TabView = IPython.DOMWidgetView.extend({
148 148 initialize: function() {
149 149 // Public constructor.
150 150 this.containers = [];
151 151 TabView.__super__.initialize.apply(this, arguments);
152 152 },
153 153
154 154 render: function(){
155 155 // Called when view is rendered.
156 156 var uuid = 'tabs'+IPython.utils.uuid();
157 157 var that = this;
158 158 this.$tabs = $('<div />', {id: uuid})
159 159 .addClass('nav')
160 160 .addClass('nav-tabs')
161 161 .appendTo(this.$el);
162 162 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
163 163 .addClass('tab-content')
164 164 .appendTo(this.$el);
165 165 this.containers = [];
166 this.update_children([], this.model.get('_children'));
167 this.model.on('change:_children', function(model, value, options) {
168 this.update_children(model.previous('_children'), value);
166 this.update_children([], this.model.get('children'));
167 this.model.on('change:children', function(model, value, options) {
168 this.update_children(model.previous('children'), value);
169 169 }, this);
170 170
171 171 // Trigger model displayed events for any models that are child to
172 172 // this model when this model is displayed.
173 this.model.on('displayed', function(){
173 this.on('displayed', function(){
174 174 that.is_displayed = true;
175 175 for (var property in that.child_views) {
176 176 if (that.child_views.hasOwnProperty(property)) {
177 that.child_views[property].model.trigger('displayed');
177 that.child_views[property].trigger('displayed');
178 178 }
179 179 }
180 180 });
181 181 },
182 182
183 183 update_children: function(old_list, new_list) {
184 184 // Called when the children list is modified.
185 185 this.do_diff(old_list,
186 186 new_list,
187 187 $.proxy(this.remove_child_model, this),
188 188 $.proxy(this.add_child_model, this));
189 189 },
190 190
191 191 remove_child_model: function(model) {
192 192 // Called when a child is removed from children list.
193 var view = this.child_views[model.id];
193 var view = this.pop_child_view(model);
194 194 this.containers.splice(view.parent_tab.tab_text_index, 1);
195 195 view.parent_tab.remove();
196 196 view.parent_container.remove();
197 197 view.remove();
198 this.delete_child_view(model);
199 198 },
200 199
201 200 add_child_model: function(model) {
202 201 // Called when a child is added to children list.
203 202 var view = this.create_child_view(model);
204 203 var index = this.containers.length;
205 204 var uuid = IPython.utils.uuid();
206 205
207 206 var that = this;
208 207 var tab = $('<li />')
209 208 .css('list-style-type', 'none')
210 209 .appendTo(this.$tabs);
211 210 view.parent_tab = tab;
212 211
213 212 var tab_text = $('<a />')
214 213 .attr('href', '#' + uuid)
215 214 .attr('data-toggle', 'tab')
216 215 .text('Page ' + index)
217 216 .appendTo(tab)
218 217 .click(function (e) {
219 218
220 219 // Calling model.set will trigger all of the other views of the
221 220 // model to update.
222 221 that.model.set("selected_index", index, {updated_view: this});
223 222 that.touch();
224 223 that.select_page(index);
225 224 });
226 225 tab.tab_text_index = this.containers.push(tab_text) - 1;
227 226
228 227 var contents_div = $('<div />', {id: uuid})
229 228 .addClass('tab-pane')
230 229 .addClass('fade')
231 230 .append(view.$el)
232 231 .appendTo(this.$tab_contents);
233 232 view.parent_container = contents_div;
234 233
235 234 // Trigger the displayed event if this model is displayed.
236 235 if (this.is_displayed) {
237 model.trigger('displayed');
236 view.trigger('displayed');
238 237 }
239 238 },
240 239
241 240 update: function(options) {
242 241 // Update the contents of this view
243 242 //
244 243 // Called when the model is changed. The model may have been
245 244 // changed by another view or by a state update from the back-end.
246 245 if (options === undefined || options.updated_view != this) {
247 246 // Set tab titles
248 247 var titles = this.model.get('_titles');
249 248 var that = this;
250 249 _.each(titles, function(title, page_index) {
251 250 var tab_text = that.containers[page_index];
252 251 if (tab_text !== undefined) {
253 252 tab_text.text(title);
254 253 }
255 254 });
256 255
257 256 var selected_index = this.model.get('selected_index');
258 257 if (0 <= selected_index && selected_index < this.containers.length) {
259 258 this.select_page(selected_index);
260 259 }
261 260 }
262 261 return TabView.__super__.update.apply(this);
263 262 },
264 263
265 264 select_page: function(index) {
266 265 // Select a page.
267 266 this.$tabs.find('li')
268 267 .removeClass('active');
269 268 this.containers[index].tab('show');
270 269 },
271 270 });
272 271 WidgetManager.register_widget_view('TabView', TabView);
273 272 });
@@ -1,62 +1,33 b''
1 1 """ContainerWidget class.
2 2
3 3 Represents a container that can be used to group other widgets.
4 4 """
5 #-----------------------------------------------------------------------------
6 # Copyright (c) 2013, the IPython Development Team.
7 #
5
6 # Copyright (c) IPython Development Team.
8 7 # Distributed under the terms of the Modified BSD License.
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
12 8
13 #-----------------------------------------------------------------------------
14 # Imports
15 #-----------------------------------------------------------------------------
16 9 from .widget import DOMWidget
17 10 from IPython.utils.traitlets import Unicode, Tuple, TraitError
18 11
19 #-----------------------------------------------------------------------------
20 # Classes
21 #-----------------------------------------------------------------------------
22
23 12 class ContainerWidget(DOMWidget):
24 13 _view_name = Unicode('ContainerView', sync=True)
25 14
26 15 # Child widgets in the container.
27 16 # Using a tuple here to force reassignment to update the list.
28 17 # When a proper notifying-list trait exists, that is what should be used here.
29 children = Tuple()
30 _children = Tuple(sync=True)
31
18 children = Tuple(sync=True)
32 19
33 20 def __init__(self, **kwargs):
34 21 super(ContainerWidget, self).__init__(**kwargs)
35 22 self.on_displayed(ContainerWidget._fire_children_displayed)
36 23
37 24 def _fire_children_displayed(self):
38 for child in self._children:
25 for child in self.children:
39 26 child._handle_displayed()
40 27
41 def _children_changed(self, name, old, new):
42 """Validate children list.
43
44 Makes sure only one instance of any given model can exist in the
45 children list.
46 An excellent post on uniqifiers is available at
47 http://www.peterbe.com/plog/uniqifiers-benchmark
48 which provides the inspiration for using this implementation. Below
49 I've implemented the `f5` algorithm using Python comprehensions."""
50 if new is not None:
51 seen = {}
52 def add_item(i):
53 seen[i.model_id] = True
54 return i
55 self._children = [add_item(i) for i in new if not i.model_id in seen]
56
57 28
58 29 class PopupWidget(ContainerWidget):
59 30 _view_name = Unicode('PopupView', sync=True)
60 31
61 32 description = Unicode(sync=True)
62 33 button_text = Unicode(sync=True)
General Comments 0
You need to be logged in to leave comments. Login now