##// END OF EJS Templates
Fixed buggy behavior
Jonathan Frederic -
Show More
@@ -1,492 +1,494
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Base Widget Model and View classes
9 // Base Widget Model and View classes
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["widgets/js/manager",
17 define(["widgets/js/manager",
18 "underscore",
18 "underscore",
19 "backbone"],
19 "backbone"],
20 function(WidgetManager, _, Backbone){
20 function(WidgetManager, _, Backbone){
21
21
22 var WidgetModel = Backbone.Model.extend({
22 var WidgetModel = Backbone.Model.extend({
23 constructor: function (widget_manager, model_id, comm) {
23 constructor: function (widget_manager, model_id, comm) {
24 // Constructor
24 // Constructor
25 //
25 //
26 // Creates a WidgetModel instance.
26 // Creates a WidgetModel instance.
27 //
27 //
28 // Parameters
28 // Parameters
29 // ----------
29 // ----------
30 // widget_manager : WidgetManager instance
30 // widget_manager : WidgetManager instance
31 // model_id : string
31 // model_id : string
32 // An ID unique to this model.
32 // An ID unique to this model.
33 // comm : Comm instance (optional)
33 // comm : Comm instance (optional)
34 this.widget_manager = widget_manager;
34 this.widget_manager = widget_manager;
35 this._buffered_state_diff = {};
35 this._buffered_state_diff = {};
36 this.pending_msgs = 0;
36 this.pending_msgs = 0;
37 this.msg_buffer = null;
37 this.msg_buffer = null;
38 this.key_value_lock = null;
38 this.key_value_lock = null;
39 this.id = model_id;
39 this.id = model_id;
40 this.views = [];
40 this.views = [];
41
41
42 if (comm !== undefined) {
42 if (comm !== undefined) {
43 // Remember comm associated with the model.
43 // Remember comm associated with the model.
44 this.comm = comm;
44 this.comm = comm;
45 comm.model = this;
45 comm.model = this;
46
46
47 // Hook comm messages up to model.
47 // Hook comm messages up to model.
48 comm.on_close($.proxy(this._handle_comm_closed, this));
48 comm.on_close($.proxy(this._handle_comm_closed, this));
49 comm.on_msg($.proxy(this._handle_comm_msg, this));
49 comm.on_msg($.proxy(this._handle_comm_msg, this));
50 }
50 }
51 return Backbone.Model.apply(this);
51 return Backbone.Model.apply(this);
52 },
52 },
53
53
54 send: function (content, callbacks) {
54 send: function (content, callbacks) {
55 // Send a custom msg over the comm.
55 // Send a custom msg over the comm.
56 if (this.comm !== undefined) {
56 if (this.comm !== undefined) {
57 var data = {method: 'custom', content: content};
57 var data = {method: 'custom', content: content};
58 this.comm.send(data, callbacks);
58 this.comm.send(data, callbacks);
59 this.pending_msgs++;
59 this.pending_msgs++;
60 }
60 }
61 },
61 },
62
62
63 _handle_comm_closed: function (msg) {
63 _handle_comm_closed: function (msg) {
64 // Handle when a widget is closed.
64 // Handle when a widget is closed.
65 this.trigger('comm:close');
65 this.trigger('comm:close');
66 delete this.comm.model; // Delete ref so GC will collect widget model.
66 delete this.comm.model; // Delete ref so GC will collect widget model.
67 delete this.comm;
67 delete this.comm;
68 delete this.model_id; // Delete id from model so widget manager cleans up.
68 delete this.model_id; // Delete id from model so widget manager cleans up.
69 _.each(this.views, function(view, i) {
69 _.each(this.views, function(view, i) {
70 view.remove();
70 view.remove();
71 });
71 });
72 },
72 },
73
73
74 _handle_comm_msg: function (msg) {
74 _handle_comm_msg: function (msg) {
75 // Handle incoming comm msg.
75 // Handle incoming comm msg.
76 var method = msg.content.data.method;
76 var method = msg.content.data.method;
77 switch (method) {
77 switch (method) {
78 case 'update':
78 case 'update':
79 this.apply_update(msg.content.data.state);
79 this.apply_update(msg.content.data.state);
80 break;
80 break;
81 case 'custom':
81 case 'custom':
82 this.trigger('msg:custom', msg.content.data.content);
82 this.trigger('msg:custom', msg.content.data.content);
83 break;
83 break;
84 case 'display':
84 case 'display':
85 this.widget_manager.display_view(msg, this);
85 this.widget_manager.display_view(msg, this);
86 this.trigger('displayed');
86 this.trigger('displayed');
87 break;
87 break;
88 }
88 }
89 },
89 },
90
90
91 apply_update: function (state) {
91 apply_update: function (state) {
92 // Handle when a widget is updated via the python side.
92 // Handle when a widget is updated via the python side.
93 var that = this;
93 var that = this;
94 _.each(state, function(value, key) {
94 _.each(state, function(value, key) {
95 that.key_value_lock = [key, value];
95 that.key_value_lock = [key, value];
96 try {
96 try {
97 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
97 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
98 } finally {
98 } finally {
99 that.key_value_lock = null;
99 that.key_value_lock = null;
100 }
100 }
101 });
101 });
102 },
102 },
103
103
104 _handle_status: function (msg, callbacks) {
104 _handle_status: function (msg, callbacks) {
105 // Handle status msgs.
105 // Handle status msgs.
106
106
107 // execution_state : ('busy', 'idle', 'starting')
107 // execution_state : ('busy', 'idle', 'starting')
108 if (this.comm !== undefined) {
108 if (this.comm !== undefined) {
109 if (msg.content.execution_state ==='idle') {
109 if (msg.content.execution_state ==='idle') {
110 // Send buffer if this message caused another message to be
110 // Send buffer if this message caused another message to be
111 // throttled.
111 // throttled.
112 if (this.msg_buffer !== null &&
112 if (this.msg_buffer !== null &&
113 (this.get('msg_throttle') || 3) === this.pending_msgs) {
113 (this.get('msg_throttle') || 3) === this.pending_msgs) {
114 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
114 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
115 this.comm.send(data, callbacks);
115 this.comm.send(data, callbacks);
116 this.msg_buffer = null;
116 this.msg_buffer = null;
117 } else {
117 } else {
118 --this.pending_msgs;
118 --this.pending_msgs;
119 }
119 }
120 }
120 }
121 }
121 }
122 },
122 },
123
123
124 callbacks: function(view) {
124 callbacks: function(view) {
125 // Create msg callbacks for a comm msg.
125 // Create msg callbacks for a comm msg.
126 var callbacks = this.widget_manager.callbacks(view);
126 var callbacks = this.widget_manager.callbacks(view);
127
127
128 if (callbacks.iopub === undefined) {
128 if (callbacks.iopub === undefined) {
129 callbacks.iopub = {};
129 callbacks.iopub = {};
130 }
130 }
131
131
132 var that = this;
132 var that = this;
133 callbacks.iopub.status = function (msg) {
133 callbacks.iopub.status = function (msg) {
134 that._handle_status(msg, callbacks);
134 that._handle_status(msg, callbacks);
135 };
135 };
136 return callbacks;
136 return callbacks;
137 },
137 },
138
138
139 set: function(key, val, options) {
139 set: function(key, val, options) {
140 // Set a value.
140 // Set a value.
141 var return_value = WidgetModel.__super__.set.apply(this, arguments);
141 var return_value = WidgetModel.__super__.set.apply(this, arguments);
142
142
143 // Backbone only remembers the diff of the most recent set()
143 // Backbone only remembers the diff of the most recent set()
144 // operation. Calling set multiple times in a row results in a
144 // operation. Calling set multiple times in a row results in a
145 // loss of diff information. Here we keep our own running diff.
145 // loss of diff information. Here we keep our own running diff.
146 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
146 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
147 return return_value;
147 return return_value;
148 },
148 },
149
149
150 sync: function (method, model, options) {
150 sync: function (method, model, options) {
151 // Handle sync to the back-end. Called when a model.save() is called.
151 // Handle sync to the back-end. Called when a model.save() is called.
152
152
153 // Make sure a comm exists.
153 // Make sure a comm exists.
154 var error = options.error || function() {
154 var error = options.error || function() {
155 console.error('Backbone sync error:', arguments);
155 console.error('Backbone sync error:', arguments);
156 };
156 };
157 if (this.comm === undefined) {
157 if (this.comm === undefined) {
158 error();
158 error();
159 return false;
159 return false;
160 }
160 }
161
161
162 // Delete any key value pairs that the back-end already knows about.
162 // Delete any key value pairs that the back-end already knows about.
163 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
163 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
164 if (this.key_value_lock !== null) {
164 if (this.key_value_lock !== null) {
165 var key = this.key_value_lock[0];
165 var key = this.key_value_lock[0];
166 var value = this.key_value_lock[1];
166 var value = this.key_value_lock[1];
167 if (attrs[key] === value) {
167 if (attrs[key] === value) {
168 delete attrs[key];
168 delete attrs[key];
169 }
169 }
170 }
170 }
171
171
172 // Only sync if there are attributes to send to the back-end.
172 // Only sync if there are attributes to send to the back-end.
173 attrs = this._pack_models(attrs);
173 attrs = this._pack_models(attrs);
174 if (_.size(attrs) > 0) {
174 if (_.size(attrs) > 0) {
175
175
176 // If this message was sent via backbone itself, it will not
176 // If this message was sent via backbone itself, it will not
177 // have any callbacks. It's important that we create callbacks
177 // have any callbacks. It's important that we create callbacks
178 // so we can listen for status messages, etc...
178 // so we can listen for status messages, etc...
179 var callbacks = options.callbacks || this.callbacks();
179 var callbacks = options.callbacks || this.callbacks();
180
180
181 // Check throttle.
181 // Check throttle.
182 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
182 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
183 // The throttle has been exceeded, buffer the current msg so
183 // The throttle has been exceeded, buffer the current msg so
184 // it can be sent once the kernel has finished processing
184 // it can be sent once the kernel has finished processing
185 // some of the existing messages.
185 // some of the existing messages.
186
186
187 // Combine updates if it is a 'patch' sync, otherwise replace updates
187 // Combine updates if it is a 'patch' sync, otherwise replace updates
188 switch (method) {
188 switch (method) {
189 case 'patch':
189 case 'patch':
190 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
190 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
191 break;
191 break;
192 case 'update':
192 case 'update':
193 case 'create':
193 case 'create':
194 this.msg_buffer = attrs;
194 this.msg_buffer = attrs;
195 break;
195 break;
196 default:
196 default:
197 error();
197 error();
198 return false;
198 return false;
199 }
199 }
200 this.msg_buffer_callbacks = callbacks;
200 this.msg_buffer_callbacks = callbacks;
201
201
202 } else {
202 } else {
203 // We haven't exceeded the throttle, send the message like
203 // We haven't exceeded the throttle, send the message like
204 // normal.
204 // normal.
205 var data = {method: 'backbone', sync_data: attrs};
205 var data = {method: 'backbone', sync_data: attrs};
206 this.comm.send(data, callbacks);
206 this.comm.send(data, callbacks);
207 this.pending_msgs++;
207 this.pending_msgs++;
208 }
208 }
209 }
209 }
210 // Since the comm is a one-way communication, assume the message
210 // Since the comm is a one-way communication, assume the message
211 // arrived. Don't call success since we don't have a model back from the server
211 // arrived. Don't call success since we don't have a model back from the server
212 // this means we miss out on the 'sync' event.
212 // this means we miss out on the 'sync' event.
213 this._buffered_state_diff = {};
213 this._buffered_state_diff = {};
214 },
214 },
215
215
216 save_changes: function(callbacks) {
216 save_changes: function(callbacks) {
217 // Push this model's state to the back-end
217 // Push this model's state to the back-end
218 //
218 //
219 // This invokes a Backbone.Sync.
219 // This invokes a Backbone.Sync.
220 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
220 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
221 },
221 },
222
222
223 _pack_models: function(value) {
223 _pack_models: function(value) {
224 // Replace models with model ids recursively.
224 // Replace models with model ids recursively.
225 if (value instanceof Backbone.Model) {
225 if (value instanceof Backbone.Model) {
226 return value.id;
226 return value.id;
227
227
228 } else if ($.isArray(value)) {
228 } else if ($.isArray(value)) {
229 var packed = [];
229 var packed = [];
230 var that = this;
230 var that = this;
231 _.each(value, function(sub_value, key) {
231 _.each(value, function(sub_value, key) {
232 packed.push(that._pack_models(sub_value));
232 packed.push(that._pack_models(sub_value));
233 });
233 });
234 return packed;
234 return packed;
235
235
236 } else if (value instanceof Object) {
236 } else if (value instanceof Object) {
237 var packed = {};
237 var packed = {};
238 var that = this;
238 var that = this;
239 _.each(value, function(sub_value, key) {
239 _.each(value, function(sub_value, key) {
240 packed[key] = that._pack_models(sub_value);
240 packed[key] = that._pack_models(sub_value);
241 });
241 });
242 return packed;
242 return packed;
243
243
244 } else {
244 } else {
245 return value;
245 return value;
246 }
246 }
247 },
247 },
248
248
249 _unpack_models: function(value) {
249 _unpack_models: function(value) {
250 // Replace model ids with models recursively.
250 // Replace model ids with models recursively.
251 if ($.isArray(value)) {
251 if ($.isArray(value)) {
252 var unpacked = [];
252 var unpacked = [];
253 var that = this;
253 var that = this;
254 _.each(value, function(sub_value, key) {
254 _.each(value, function(sub_value, key) {
255 unpacked.push(that._unpack_models(sub_value));
255 unpacked.push(that._unpack_models(sub_value));
256 });
256 });
257 return unpacked;
257 return unpacked;
258
258
259 } else if (value instanceof Object) {
259 } else if (value instanceof Object) {
260 var unpacked = {};
260 var unpacked = {};
261 var that = this;
261 var that = this;
262 _.each(value, function(sub_value, key) {
262 _.each(value, function(sub_value, key) {
263 unpacked[key] = that._unpack_models(sub_value);
263 unpacked[key] = that._unpack_models(sub_value);
264 });
264 });
265 return unpacked;
265 return unpacked;
266
266
267 } else {
267 } else {
268 var model = this.widget_manager.get_model(value);
268 var model = this.widget_manager.get_model(value);
269 if (model) {
269 if (model) {
270 return model;
270 return model;
271 } else {
271 } else {
272 return value;
272 return value;
273 }
273 }
274 }
274 }
275 },
275 },
276
276
277 });
277 });
278 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
278 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
279
279
280
280
281 var WidgetView = Backbone.View.extend({
281 var WidgetView = Backbone.View.extend({
282 initialize: function(parameters) {
282 initialize: function(parameters) {
283 // Public constructor.
283 // Public constructor.
284 this.model.on('change',this.update,this);
284 this.model.on('change',this.update,this);
285 this.options = parameters.options;
285 this.options = parameters.options;
286 this.child_model_views = {};
286 this.child_model_views = {};
287 this.child_views = {};
287 this.child_views = {};
288 this.model.views.push(this);
288 this.model.views.push(this);
289 },
289 },
290
290
291 update: function(){
291 update: function(){
292 // Triggered on model change.
292 // Triggered on model change.
293 //
293 //
294 // Update view to be consistent with this.model
294 // Update view to be consistent with this.model
295 },
295 },
296
296
297 create_child_view: function(child_model, options) {
297 create_child_view: function(child_model, options) {
298 // Create and return a child view.
298 // Create and return a child view.
299 //
299 //
300 // -given a model and (optionally) a view name if the view name is
300 // -given a model and (optionally) a view name if the view name is
301 // not given, it defaults to the model's default view attribute.
301 // not given, it defaults to the model's default view attribute.
302
302
303 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
303 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
304 // it would be great to have the widget manager add the cell metadata
304 // it would be great to have the widget manager add the cell metadata
305 // to the subview without having to add it here.
305 // to the subview without having to add it here.
306 var child_view = this.model.widget_manager.create_view(child_model, options || {}, this);
306 var child_view = this.model.widget_manager.create_view(child_model, options || {}, this);
307
307
308 // Associate the view id with the model id.
308 // Associate the view id with the model id.
309 if (this.child_model_views[child_model.id] === undefined) {
309 if (this.child_model_views[child_model.id] === undefined) {
310 this.child_model_views[child_model.id] = [];
310 this.child_model_views[child_model.id] = [];
311 }
311 }
312 this.child_model_views[child_model.id].push(child_view.id);
312 this.child_model_views[child_model.id].push(child_view.id);
313
313
314 // Remember the view by id.
314 // Remember the view by id.
315 this.child_views[child_view.id] = child_view;
315 this.child_views[child_view.id] = child_view;
316 return child_view;
316 return child_view;
317 },
317 },
318
318
319 delete_child_view: function(child_model, options) {
319 delete_child_view: function(child_model, options) {
320 // Delete a child view that was previously created using create_child_view.
320 // Delete a child view that was previously created using create_child_view.
321 var view_ids = this.child_model_views[child_model.id];
321 var view_ids = this.child_model_views[child_model.id];
322 if (view_ids !== undefined) {
322 if (view_ids !== undefined) {
323
323
324 // Remove every view associate with the model id.
324 // Only delete the first view in the list.
325 for (var i =0; i < view_ids.length; i++) {
325 var view_id = view_ids[0];
326 var view_id = view_ids[i];
326 var view = this.child_views[view_id];
327 var view = this.child_views[view_id];
327 delete this.child_views[view_id];
328 views.remove();
328 delete view_ids[0];
329 delete this.child_views[view_id];
329 child_model.views.pop(view);
330 child_model.views.pop(view);
330
331 // Remove the view list specific to this model if it is empty.
332 if (view_ids.length === 0) {
333 delete this.child_model_views[child_model.id];
331 }
334 }
332
335 return view;
333 // Remove the view list specific to this model.
334 delete this.child_model_views[child_model.id];
335 }
336 }
337 return null;
336 },
338 },
337
339
338 do_diff: function(old_list, new_list, removed_callback, added_callback) {
340 do_diff: function(old_list, new_list, removed_callback, added_callback) {
339 // Difference a changed list and call remove and add callbacks for
341 // Difference a changed list and call remove and add callbacks for
340 // each removed and added item in the new list.
342 // each removed and added item in the new list.
341 //
343 //
342 // Parameters
344 // Parameters
343 // ----------
345 // ----------
344 // old_list : array
346 // old_list : array
345 // new_list : array
347 // new_list : array
346 // removed_callback : Callback(item)
348 // removed_callback : Callback(item)
347 // Callback that is called for each item removed.
349 // Callback that is called for each item removed.
348 // added_callback : Callback(item)
350 // added_callback : Callback(item)
349 // Callback that is called for each item added.
351 // Callback that is called for each item added.
350
352
351
353
352 // removed items
354 // removed items
353 _.each(this.difference(old_list, new_list), function(item, index, list) {
355 _.each(this.difference(old_list, new_list), function(item, index, list) {
354 removed_callback(item);
356 removed_callback(item);
355 }, this);
357 }, this);
356
358
357 // added items
359 // added items
358 _.each(this.difference(new_list, old_list), function(item, index, list) {
360 _.each(this.difference(new_list, old_list), function(item, index, list) {
359 added_callback(item);
361 added_callback(item);
360 }, this);
362 }, this);
361 },
363 },
362
364
363 difference: function(a, b) {
365 difference: function(a, b) {
364 // Calculate the difference of two lists by contents.
366 // Calculate the difference of two lists by contents.
365 //
367 //
366 // This function is like the underscore difference function
368 // This function is like the underscore difference function
367 // except it will not fail when given a list with duplicates.
369 // except it will not fail when given a list with duplicates.
368 // i.e.:
370 // i.e.:
369 // diff([1, 2, 2, 3], [3, 2])
371 // diff([1, 2, 2, 3], [3, 2])
370 // Underscores results:
372 // Underscores results:
371 // [1]
373 // [1]
372 // This method:
374 // This method:
373 // [1, 2]
375 // [1, 2]
374 var contents = a.slice(0);
376 var contents = a.slice(0);
375 var found_index;
377 var found_index;
376 for (var i = 0; i < b.length; i++) {
378 for (var i = 0; i < b.length; i++) {
377 found_index = _.indexOf(contents, b[i]);
379 found_index = _.indexOf(contents, b[i]);
378 if (found_index >= 0) {
380 if (found_index >= 0) {
379 contents.splice(found_index, 1);
381 contents.splice(found_index, 1);
380 }
382 }
381 }
383 }
382 return contents;
384 return contents;
383 },
385 },
384
386
385 callbacks: function(){
387 callbacks: function(){
386 // Create msg callbacks for a comm msg.
388 // Create msg callbacks for a comm msg.
387 return this.model.callbacks(this);
389 return this.model.callbacks(this);
388 },
390 },
389
391
390 render: function(){
392 render: function(){
391 // Render the view.
393 // Render the view.
392 //
394 //
393 // By default, this is only called the first time the view is created
395 // By default, this is only called the first time the view is created
394 },
396 },
395
397
396 send: function (content) {
398 send: function (content) {
397 // Send a custom msg associated with this view.
399 // Send a custom msg associated with this view.
398 this.model.send(content, this.callbacks());
400 this.model.send(content, this.callbacks());
399 },
401 },
400
402
401 touch: function () {
403 touch: function () {
402 this.model.save_changes(this.callbacks());
404 this.model.save_changes(this.callbacks());
403 },
405 },
404 });
406 });
405
407
406
408
407 var DOMWidgetView = WidgetView.extend({
409 var DOMWidgetView = WidgetView.extend({
408 initialize: function (options) {
410 initialize: function (options) {
409 // Public constructor
411 // Public constructor
410
412
411 // In the future we may want to make changes more granular
413 // In the future we may want to make changes more granular
412 // (e.g., trigger on visible:change).
414 // (e.g., trigger on visible:change).
413 this.model.on('change', this.update, this);
415 this.model.on('change', this.update, this);
414 this.model.on('msg:custom', this.on_msg, this);
416 this.model.on('msg:custom', this.on_msg, this);
415 DOMWidgetView.__super__.initialize.apply(this, arguments);
417 DOMWidgetView.__super__.initialize.apply(this, arguments);
416 },
418 },
417
419
418 on_msg: function(msg) {
420 on_msg: function(msg) {
419 // Handle DOM specific msgs.
421 // Handle DOM specific msgs.
420 switch(msg.msg_type) {
422 switch(msg.msg_type) {
421 case 'add_class':
423 case 'add_class':
422 this.add_class(msg.selector, msg.class_list);
424 this.add_class(msg.selector, msg.class_list);
423 break;
425 break;
424 case 'remove_class':
426 case 'remove_class':
425 this.remove_class(msg.selector, msg.class_list);
427 this.remove_class(msg.selector, msg.class_list);
426 break;
428 break;
427 }
429 }
428 },
430 },
429
431
430 add_class: function (selector, class_list) {
432 add_class: function (selector, class_list) {
431 // Add a DOM class to an element.
433 // Add a DOM class to an element.
432 this._get_selector_element(selector).addClass(class_list);
434 this._get_selector_element(selector).addClass(class_list);
433 },
435 },
434
436
435 remove_class: function (selector, class_list) {
437 remove_class: function (selector, class_list) {
436 // Remove a DOM class from an element.
438 // Remove a DOM class from an element.
437 this._get_selector_element(selector).removeClass(class_list);
439 this._get_selector_element(selector).removeClass(class_list);
438 },
440 },
439
441
440 update: function () {
442 update: function () {
441 // Update the contents of this view
443 // Update the contents of this view
442 //
444 //
443 // Called when the model is changed. The model may have been
445 // Called when the model is changed. The model may have been
444 // changed by another view or by a state update from the back-end.
446 // changed by another view or by a state update from the back-end.
445 // The very first update seems to happen before the element is
447 // The very first update seems to happen before the element is
446 // finished rendering so we use setTimeout to give the element time
448 // finished rendering so we use setTimeout to give the element time
447 // to render
449 // to render
448 var e = this.$el;
450 var e = this.$el;
449 var visible = this.model.get('visible');
451 var visible = this.model.get('visible');
450 setTimeout(function() {e.toggle(visible);},0);
452 setTimeout(function() {e.toggle(visible);},0);
451
453
452 var css = this.model.get('_css');
454 var css = this.model.get('_css');
453 if (css === undefined) {return;}
455 if (css === undefined) {return;}
454 var that = this;
456 var that = this;
455 _.each(css, function(css_traits, selector){
457 _.each(css, function(css_traits, selector){
456 // Apply the css traits to all elements that match the selector.
458 // Apply the css traits to all elements that match the selector.
457 var elements = that._get_selector_element(selector);
459 var elements = that._get_selector_element(selector);
458 if (elements.length > 0) {
460 if (elements.length > 0) {
459 _.each(css_traits, function(css_value, css_key){
461 _.each(css_traits, function(css_value, css_key){
460 elements.css(css_key, css_value);
462 elements.css(css_key, css_value);
461 });
463 });
462 }
464 }
463 });
465 });
464 },
466 },
465
467
466 _get_selector_element: function (selector) {
468 _get_selector_element: function (selector) {
467 // Get the elements via the css selector.
469 // Get the elements via the css selector.
468
470
469 // If the selector is blank, apply the style to the $el_to_style
471 // If the selector is blank, apply the style to the $el_to_style
470 // element. If the $el_to_style element is not defined, use apply
472 // element. If the $el_to_style element is not defined, use apply
471 // the style to the view's element.
473 // the style to the view's element.
472 var elements;
474 var elements;
473 if (!selector) {
475 if (!selector) {
474 if (this.$el_to_style === undefined) {
476 if (this.$el_to_style === undefined) {
475 elements = this.$el;
477 elements = this.$el;
476 } else {
478 } else {
477 elements = this.$el_to_style;
479 elements = this.$el_to_style;
478 }
480 }
479 } else {
481 } else {
480 elements = this.$el.find(selector);
482 elements = this.$el.find(selector);
481 }
483 }
482 return elements;
484 return elements;
483 },
485 },
484 });
486 });
485
487
486 IPython.WidgetModel = WidgetModel;
488 IPython.WidgetModel = WidgetModel;
487 IPython.WidgetView = WidgetView;
489 IPython.WidgetView = WidgetView;
488 IPython.DOMWidgetView = DOMWidgetView;
490 IPython.DOMWidgetView = DOMWidgetView;
489
491
490 // Pass through WidgetManager namespace.
492 // Pass through WidgetManager namespace.
491 return WidgetManager;
493 return WidgetManager;
492 });
494 });
@@ -1,325 +1,323
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // ContainerWidget
9 // ContainerWidget
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["widgets/js/widget"], function(WidgetManager) {
17 define(["widgets/js/widget"], function(WidgetManager) {
18
18
19 var ContainerView = IPython.DOMWidgetView.extend({
19 var ContainerView = IPython.DOMWidgetView.extend({
20 render: function(){
20 render: function(){
21 // Called when view is rendered.
21 // Called when view is rendered.
22 this.$el.addClass('widget-container')
22 this.$el.addClass('widget-container')
23 .addClass('vbox');
23 .addClass('vbox');
24 this.children={};
24 this.children={};
25 this.update_children([], this.model.get('_children'));
25 this.update_children([], this.model.get('children'));
26 this.model.on('change:_children', function(model, value, options) {
26 this.model.on('change:children', function(model, value, options) {
27 this.update_children(model.previous('_children'), value);
27 this.update_children(model.previous('children'), value);
28 }, this);
28 }, this);
29 this.update();
29 this.update();
30
30
31 // Trigger model displayed events for any models that are child to
31 // Trigger model displayed events for any models that are child to
32 // this model when this model is displayed.
32 // this model when this model is displayed.
33 var that = this;
33 var that = this;
34 this.model.on('displayed', function(){
34 this.model.on('displayed', function(){
35 that.is_displayed = true;
35 that.is_displayed = true;
36 for (var property in that.child_views) {
36 for (var property in that.child_views) {
37 if (that.child_views.hasOwnProperty(property)) {
37 if (that.child_views.hasOwnProperty(property)) {
38 that.child_views[property].model.trigger('displayed');
38 that.child_views[property].model.trigger('displayed');
39 }
39 }
40 }
40 }
41 });
41 });
42 },
42 },
43
43
44 update_children: function(old_list, new_list) {
44 update_children: function(old_list, new_list) {
45 // Called when the children list changes.
45 // Called when the children list changes.
46 this.do_diff(old_list,
46 this.do_diff(old_list,
47 new_list,
47 new_list,
48 $.proxy(this.remove_child_model, this),
48 $.proxy(this.remove_child_model, this),
49 $.proxy(this.add_child_model, this));
49 $.proxy(this.add_child_model, this));
50 },
50 },
51
51
52 remove_child_model: function(model) {
52 remove_child_model: function(model) {
53 // Called when a model is removed from the children list.
53 // Called when a model is removed from the children list.
54 this.child_views[model.id].remove();
54 this.delete_child_view(model).remove();
55 this.delete_child_view(model);
56 },
55 },
57
56
58 add_child_model: function(model) {
57 add_child_model: function(model) {
59 // Called when a model is added to the children list.
58 // Called when a model is added to the children list.
60 var view = this.create_child_view(model);
59 var view = this.create_child_view(model);
61 this.$el.append(view.$el);
60 this.$el.append(view.$el);
62
61
63 // Trigger the displayed event if this model is displayed.
62 // Trigger the displayed event if this model is displayed.
64 if (this.is_displayed) {
63 if (this.is_displayed) {
65 model.trigger('displayed');
64 model.trigger('displayed');
66 }
65 }
67 },
66 },
68
67
69 update: function(){
68 update: function(){
70 // Update the contents of this view
69 // Update the contents of this view
71 //
70 //
72 // Called when the model is changed. The model may have been
71 // Called when the model is changed. The model may have been
73 // changed by another view or by a state update from the back-end.
72 // changed by another view or by a state update from the back-end.
74 return ContainerView.__super__.update.apply(this);
73 return ContainerView.__super__.update.apply(this);
75 },
74 },
76 });
75 });
77
76
78 WidgetManager.register_widget_view('ContainerView', ContainerView);
77 WidgetManager.register_widget_view('ContainerView', ContainerView);
79
78
80 var PopupView = IPython.DOMWidgetView.extend({
79 var PopupView = IPython.DOMWidgetView.extend({
81 render: function(){
80 render: function(){
82 // Called when view is rendered.
81 // Called when view is rendered.
83 var that = this;
82 var that = this;
84 this.children={};
83 this.children={};
85
84
86 this.$el.on("remove", function(){
85 this.$el.on("remove", function(){
87 that.$backdrop.remove();
86 that.$backdrop.remove();
88 });
87 });
89 this.$backdrop = $('<div />')
88 this.$backdrop = $('<div />')
90 .appendTo($('#notebook-container'))
89 .appendTo($('#notebook-container'))
91 .addClass('modal-dialog')
90 .addClass('modal-dialog')
92 .css('position', 'absolute')
91 .css('position', 'absolute')
93 .css('left', '0px')
92 .css('left', '0px')
94 .css('top', '0px');
93 .css('top', '0px');
95 this.$window = $('<div />')
94 this.$window = $('<div />')
96 .appendTo(this.$backdrop)
95 .appendTo(this.$backdrop)
97 .addClass('modal-content widget-modal')
96 .addClass('modal-content widget-modal')
98 .mousedown(function(){
97 .mousedown(function(){
99 that.bring_to_front();
98 that.bring_to_front();
100 });
99 });
101
100
102 // Set the elements array since the this.$window element is not child
101 // Set the elements array since the this.$window element is not child
103 // of this.$el and the parent widget manager or other widgets may
102 // of this.$el and the parent widget manager or other widgets may
104 // need to know about all of the top-level widgets. The IPython
103 // need to know about all of the top-level widgets. The IPython
105 // widget manager uses this to register the elements with the
104 // widget manager uses this to register the elements with the
106 // keyboard manager.
105 // keyboard manager.
107 this.additional_elements = [this.$window];
106 this.additional_elements = [this.$window];
108
107
109 this.$title_bar = $('<div />')
108 this.$title_bar = $('<div />')
110 .addClass('popover-title')
109 .addClass('popover-title')
111 .appendTo(this.$window)
110 .appendTo(this.$window)
112 .mousedown(function(){
111 .mousedown(function(){
113 that.bring_to_front();
112 that.bring_to_front();
114 });
113 });
115 this.$close = $('<button />')
114 this.$close = $('<button />')
116 .addClass('close icon-remove')
115 .addClass('close icon-remove')
117 .css('margin-left', '5px')
116 .css('margin-left', '5px')
118 .appendTo(this.$title_bar)
117 .appendTo(this.$title_bar)
119 .click(function(){
118 .click(function(){
120 that.hide();
119 that.hide();
121 event.stopPropagation();
120 event.stopPropagation();
122 });
121 });
123 this.$minimize = $('<button />')
122 this.$minimize = $('<button />')
124 .addClass('close icon-arrow-down')
123 .addClass('close icon-arrow-down')
125 .appendTo(this.$title_bar)
124 .appendTo(this.$title_bar)
126 .click(function(){
125 .click(function(){
127 that.popped_out = !that.popped_out;
126 that.popped_out = !that.popped_out;
128 if (!that.popped_out) {
127 if (!that.popped_out) {
129 that.$minimize
128 that.$minimize
130 .removeClass('icon-arrow-down')
129 .removeClass('icon-arrow-down')
131 .addClass('icon-arrow-up');
130 .addClass('icon-arrow-up');
132
131
133 that.$window
132 that.$window
134 .draggable('destroy')
133 .draggable('destroy')
135 .resizable('destroy')
134 .resizable('destroy')
136 .removeClass('widget-modal modal-content')
135 .removeClass('widget-modal modal-content')
137 .addClass('docked-widget-modal')
136 .addClass('docked-widget-modal')
138 .detach()
137 .detach()
139 .insertBefore(that.$show_button);
138 .insertBefore(that.$show_button);
140 that.$show_button.hide();
139 that.$show_button.hide();
141 that.$close.hide();
140 that.$close.hide();
142 } else {
141 } else {
143 that.$minimize
142 that.$minimize
144 .addClass('icon-arrow-down')
143 .addClass('icon-arrow-down')
145 .removeClass('icon-arrow-up');
144 .removeClass('icon-arrow-up');
146
145
147 that.$window
146 that.$window
148 .removeClass('docked-widget-modal')
147 .removeClass('docked-widget-modal')
149 .addClass('widget-modal modal-content')
148 .addClass('widget-modal modal-content')
150 .detach()
149 .detach()
151 .appendTo(that.$backdrop)
150 .appendTo(that.$backdrop)
152 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
151 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
153 .resizable()
152 .resizable()
154 .children('.ui-resizable-handle').show();
153 .children('.ui-resizable-handle').show();
155 that.show();
154 that.show();
156 that.$show_button.show();
155 that.$show_button.show();
157 that.$close.show();
156 that.$close.show();
158 }
157 }
159 event.stopPropagation();
158 event.stopPropagation();
160 });
159 });
161 this.$title = $('<div />')
160 this.$title = $('<div />')
162 .addClass('widget-modal-title')
161 .addClass('widget-modal-title')
163 .html("&nbsp;")
162 .html("&nbsp;")
164 .appendTo(this.$title_bar);
163 .appendTo(this.$title_bar);
165 this.$body = $('<div />')
164 this.$body = $('<div />')
166 .addClass('modal-body')
165 .addClass('modal-body')
167 .addClass('widget-modal-body')
166 .addClass('widget-modal-body')
168 .addClass('widget-container')
167 .addClass('widget-container')
169 .addClass('vbox')
168 .addClass('vbox')
170 .appendTo(this.$window);
169 .appendTo(this.$window);
171
170
172 this.$show_button = $('<button />')
171 this.$show_button = $('<button />')
173 .html("&nbsp;")
172 .html("&nbsp;")
174 .addClass('btn btn-info widget-modal-show')
173 .addClass('btn btn-info widget-modal-show')
175 .appendTo(this.$el)
174 .appendTo(this.$el)
176 .click(function(){
175 .click(function(){
177 that.show();
176 that.show();
178 });
177 });
179
178
180 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
179 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
181 this.$window.resizable();
180 this.$window.resizable();
182 this.$window.on('resize', function(){
181 this.$window.on('resize', function(){
183 that.$body.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
182 that.$body.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
184 });
183 });
185
184
186 this.$el_to_style = this.$body;
185 this.$el_to_style = this.$body;
187 this._shown_once = false;
186 this._shown_once = false;
188 this.popped_out = true;
187 this.popped_out = true;
189
188
190 this.update_children([], this.model.get('_children'));
189 this.update_children([], this.model.get('children'));
191 this.model.on('change:_children', function(model, value, options) {
190 this.model.on('change:children', function(model, value, options) {
192 this.update_children(model.previous('_children'), value);
191 this.update_children(model.previous('children'), value);
193 }, this);
192 }, this);
194 this.update();
193 this.update();
195
194
196 // Trigger model displayed events for any models that are child to
195 // Trigger model displayed events for any models that are child to
197 // this model when this model is displayed.
196 // this model when this model is displayed.
198 this.model.on('displayed', function(){
197 this.model.on('displayed', function(){
199 that.is_displayed = true;
198 that.is_displayed = true;
200 for (var property in that.child_views) {
199 for (var property in that.child_views) {
201 if (that.child_views.hasOwnProperty(property)) {
200 if (that.child_views.hasOwnProperty(property)) {
202 that.child_views[property].model.trigger('displayed');
201 that.child_views[property].model.trigger('displayed');
203 }
202 }
204 }
203 }
205 });
204 });
206 },
205 },
207
206
208 hide: function() {
207 hide: function() {
209 // Called when the modal hide button is clicked.
208 // Called when the modal hide button is clicked.
210 this.$window.hide();
209 this.$window.hide();
211 this.$show_button.removeClass('btn-info');
210 this.$show_button.removeClass('btn-info');
212 },
211 },
213
212
214 show: function() {
213 show: function() {
215 // Called when the modal show button is clicked.
214 // Called when the modal show button is clicked.
216 this.$show_button.addClass('btn-info');
215 this.$show_button.addClass('btn-info');
217 this.$window.show();
216 this.$window.show();
218 if (this.popped_out) {
217 if (this.popped_out) {
219 this.$window.css("positon", "absolute");
218 this.$window.css("positon", "absolute");
220 this.$window.css("top", "0px");
219 this.$window.css("top", "0px");
221 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
220 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
222 $(window).scrollLeft()) + "px");
221 $(window).scrollLeft()) + "px");
223 this.bring_to_front();
222 this.bring_to_front();
224 }
223 }
225 },
224 },
226
225
227 bring_to_front: function() {
226 bring_to_front: function() {
228 // Make the modal top-most, z-ordered about the other modals.
227 // Make the modal top-most, z-ordered about the other modals.
229 var $widget_modals = $(".widget-modal");
228 var $widget_modals = $(".widget-modal");
230 var max_zindex = 0;
229 var max_zindex = 0;
231 $widget_modals.each(function (index, el){
230 $widget_modals.each(function (index, el){
232 var zindex = parseInt($(el).css('z-index'));
231 var zindex = parseInt($(el).css('z-index'));
233 if (!isNaN(zindex)) {
232 if (!isNaN(zindex)) {
234 max_zindex = Math.max(max_zindex, zindex);
233 max_zindex = Math.max(max_zindex, zindex);
235 }
234 }
236 });
235 });
237
236
238 // Start z-index of widget modals at 2000
237 // Start z-index of widget modals at 2000
239 max_zindex = Math.max(max_zindex, 2000);
238 max_zindex = Math.max(max_zindex, 2000);
240
239
241 $widget_modals.each(function (index, el){
240 $widget_modals.each(function (index, el){
242 $el = $(el);
241 $el = $(el);
243 if (max_zindex == parseInt($el.css('z-index'))) {
242 if (max_zindex == parseInt($el.css('z-index'))) {
244 $el.css('z-index', max_zindex - 1);
243 $el.css('z-index', max_zindex - 1);
245 }
244 }
246 });
245 });
247 this.$window.css('z-index', max_zindex);
246 this.$window.css('z-index', max_zindex);
248 },
247 },
249
248
250 update_children: function(old_list, new_list) {
249 update_children: function(old_list, new_list) {
251 // Called when the children list is modified.
250 // Called when the children list is modified.
252 this.do_diff(old_list,
251 this.do_diff(old_list,
253 new_list,
252 new_list,
254 $.proxy(this.remove_child_model, this),
253 $.proxy(this.remove_child_model, this),
255 $.proxy(this.add_child_model, this));
254 $.proxy(this.add_child_model, this));
256 },
255 },
257
256
258 remove_child_model: function(model) {
257 remove_child_model: function(model) {
259 // Called when a child is removed from children list.
258 // Called when a child is removed from children list.
260 this.child_views[model.id].remove();
259 this.delete_child_view(model).remove();
261 this.delete_child_view(model);
262 },
260 },
263
261
264 add_child_model: function(model) {
262 add_child_model: function(model) {
265 // Called when a child is added to children list.
263 // Called when a child is added to children list.
266 var view = this.create_child_view(model);
264 var view = this.create_child_view(model);
267 this.$body.append(view.$el);
265 this.$body.append(view.$el);
268
266
269 // Trigger the displayed event if this model is displayed.
267 // Trigger the displayed event if this model is displayed.
270 if (this.is_displayed) {
268 if (this.is_displayed) {
271 model.trigger('displayed');
269 model.trigger('displayed');
272 }
270 }
273 },
271 },
274
272
275 update: function(){
273 update: function(){
276 // Update the contents of this view
274 // Update the contents of this view
277 //
275 //
278 // Called when the model is changed. The model may have been
276 // Called when the model is changed. The model may have been
279 // changed by another view or by a state update from the back-end.
277 // changed by another view or by a state update from the back-end.
280 var description = this.model.get('description');
278 var description = this.model.get('description');
281 if (description.trim().length === 0) {
279 if (description.trim().length === 0) {
282 this.$title.html("&nbsp;"); // Preserve title height
280 this.$title.html("&nbsp;"); // Preserve title height
283 } else {
281 } else {
284 this.$title.text(description);
282 this.$title.text(description);
285 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
283 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
286 }
284 }
287
285
288 var button_text = this.model.get('button_text');
286 var button_text = this.model.get('button_text');
289 if (button_text.trim().length === 0) {
287 if (button_text.trim().length === 0) {
290 this.$show_button.html("&nbsp;"); // Preserve button height
288 this.$show_button.html("&nbsp;"); // Preserve button height
291 } else {
289 } else {
292 this.$show_button.text(button_text);
290 this.$show_button.text(button_text);
293 }
291 }
294
292
295 if (!this._shown_once) {
293 if (!this._shown_once) {
296 this._shown_once = true;
294 this._shown_once = true;
297 this.show();
295 this.show();
298 }
296 }
299
297
300 return PopupView.__super__.update.apply(this);
298 return PopupView.__super__.update.apply(this);
301 },
299 },
302
300
303 _get_selector_element: function(selector) {
301 _get_selector_element: function(selector) {
304 // Get an element view a 'special' jquery selector. (see widget.js)
302 // Get an element view a 'special' jquery selector. (see widget.js)
305 //
303 //
306 // Since the modal actually isn't within the $el in the DOM, we need to extend
304 // Since the modal actually isn't within the $el in the DOM, we need to extend
307 // the selector logic to allow the user to set css on the modal if need be.
305 // the selector logic to allow the user to set css on the modal if need be.
308 // The convention used is:
306 // The convention used is:
309 // "modal" - select the modal div
307 // "modal" - select the modal div
310 // "modal [selector]" - select element(s) within the modal div.
308 // "modal [selector]" - select element(s) within the modal div.
311 // "[selector]" - select elements within $el
309 // "[selector]" - select elements within $el
312 // "" - select the $el_to_style
310 // "" - select the $el_to_style
313 if (selector.substring(0, 5) == 'modal') {
311 if (selector.substring(0, 5) == 'modal') {
314 if (selector == 'modal') {
312 if (selector == 'modal') {
315 return this.$window;
313 return this.$window;
316 } else {
314 } else {
317 return this.$window.find(selector.substring(6));
315 return this.$window.find(selector.substring(6));
318 }
316 }
319 } else {
317 } else {
320 return PopupView.__super__._get_selector_element.apply(this, [selector]);
318 return PopupView.__super__._get_selector_element.apply(this, [selector]);
321 }
319 }
322 },
320 },
323 });
321 });
324 WidgetManager.register_widget_view('PopupView', PopupView);
322 WidgetManager.register_widget_view('PopupView', PopupView);
325 });
323 });
@@ -1,273 +1,272
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // SelectionContainerWidget
9 // SelectionContainerWidget
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["widgets/js/widget"], function(WidgetManager){
17 define(["widgets/js/widget"], function(WidgetManager){
18
18
19 var AccordionView = IPython.DOMWidgetView.extend({
19 var AccordionView = IPython.DOMWidgetView.extend({
20 render: function(){
20 render: function(){
21 // Called when view is rendered.
21 // Called when view is rendered.
22 var guid = 'panel-group' + IPython.utils.uuid();
22 var guid = 'panel-group' + IPython.utils.uuid();
23 this.$el
23 this.$el
24 .attr('id', guid)
24 .attr('id', guid)
25 .addClass('panel-group');
25 .addClass('panel-group');
26 this.containers = [];
26 this.containers = [];
27 this.model_containers = {};
27 this.model_containers = {};
28 this.update_children([], this.model.get('_children'));
28 this.update_children([], this.model.get('children'));
29 this.model.on('change:_children', function(model, value, options) {
29 this.model.on('change:children', function(model, value, options) {
30 this.update_children(model.previous('_children'), value);
30 this.update_children(model.previous('children'), value);
31 }, this);
31 }, this);
32 this.model.on('change:selected_index', function(model, value, options) {
32 this.model.on('change:selected_index', function(model, value, options) {
33 this.update_selected_index(model.previous('selected_index'), value, options);
33 this.update_selected_index(model.previous('selected_index'), value, options);
34 }, this);
34 }, this);
35 this.model.on('change:_titles', function(model, value, options) {
35 this.model.on('change:_titles', function(model, value, options) {
36 this.update_titles(value);
36 this.update_titles(value);
37 }, this);
37 }, this);
38 var that = this;
38 var that = this;
39 this.model.on('displayed', function() {
39 this.model.on('displayed', function() {
40 this.update_titles();
40 this.update_titles();
41 // Trigger model displayed events for any models that are child to
41 // Trigger model displayed events for any models that are child to
42 // this model when this model is displayed.
42 // this model when this model is displayed.
43 that.is_displayed = true;
43 that.is_displayed = true;
44 for (var property in that.child_views) {
44 for (var property in that.child_views) {
45 if (that.child_views.hasOwnProperty(property)) {
45 if (that.child_views.hasOwnProperty(property)) {
46 that.child_views[property].model.trigger('displayed');
46 that.child_views[property].model.trigger('displayed');
47 }
47 }
48 }
48 }
49 }, this);
49 }, this);
50 },
50 },
51
51
52 update_titles: function(titles) {
52 update_titles: function(titles) {
53 // Set tab titles
53 // Set tab titles
54 if (!titles) {
54 if (!titles) {
55 titles = this.model.get('_titles');
55 titles = this.model.get('_titles');
56 }
56 }
57
57
58 var that = this;
58 var that = this;
59 _.each(titles, function(title, page_index) {
59 _.each(titles, function(title, page_index) {
60 var accordian = that.containers[page_index];
60 var accordian = that.containers[page_index];
61 if (accordian !== undefined) {
61 if (accordian !== undefined) {
62 accordian
62 accordian
63 .find('.panel-heading')
63 .find('.panel-heading')
64 .find('.accordion-toggle')
64 .find('.accordion-toggle')
65 .text(title);
65 .text(title);
66 }
66 }
67 });
67 });
68 },
68 },
69
69
70 update_selected_index: function(old_index, new_index, options) {
70 update_selected_index: function(old_index, new_index, options) {
71 // Only update the selection if the selection wasn't triggered
71 // Only update the selection if the selection wasn't triggered
72 // by the front-end. It must be triggered by the back-end.
72 // by the front-end. It must be triggered by the back-end.
73 if (options === undefined || options.updated_view != this) {
73 if (options === undefined || options.updated_view != this) {
74 this.containers[old_index].find('.panel-collapse').collapse('hide');
74 this.containers[old_index].find('.panel-collapse').collapse('hide');
75 if (0 <= new_index && new_index < this.containers.length) {
75 if (0 <= new_index && new_index < this.containers.length) {
76 this.containers[new_index].find('.panel-collapse').collapse('show');
76 this.containers[new_index].find('.panel-collapse').collapse('show');
77 }
77 }
78 }
78 }
79 },
79 },
80
80
81 update_children: function(old_list, new_list) {
81 update_children: function(old_list, new_list) {
82 // Called when the children list is modified.
82 // Called when the children list is modified.
83 this.do_diff(old_list,
83 this.do_diff(old_list,
84 new_list,
84 new_list,
85 $.proxy(this.remove_child_model, this),
85 $.proxy(this.remove_child_model, this),
86 $.proxy(this.add_child_model, this));
86 $.proxy(this.add_child_model, this));
87 },
87 },
88
88
89 remove_child_model: function(model) {
89 remove_child_model: function(model) {
90 // Called when a child is removed from children list.
90 // Called when a child is removed from children list.
91 var accordion_group = this.model_containers[model.id];
91 var accordion_group = this.model_containers[model.id];
92 this.containers.splice(accordion_group.container_index, 1);
92 this.containers.splice(accordion_group.container_index, 1);
93 delete this.model_containers[model.id];
93 delete this.model_containers[model.id];
94 accordion_group.remove();
94 accordion_group.remove();
95 this.delete_child_view(model);
95 this.delete_child_view(model);
96 },
96 },
97
97
98 add_child_model: function(model) {
98 add_child_model: function(model) {
99 // Called when a child is added to children list.
99 // Called when a child is added to children list.
100 var view = this.create_child_view(model);
100 var view = this.create_child_view(model);
101 var index = this.containers.length;
101 var index = this.containers.length;
102 var uuid = IPython.utils.uuid();
102 var uuid = IPython.utils.uuid();
103 var accordion_group = $('<div />')
103 var accordion_group = $('<div />')
104 .addClass('panel panel-default')
104 .addClass('panel panel-default')
105 .appendTo(this.$el);
105 .appendTo(this.$el);
106 var accordion_heading = $('<div />')
106 var accordion_heading = $('<div />')
107 .addClass('panel-heading')
107 .addClass('panel-heading')
108 .appendTo(accordion_group);
108 .appendTo(accordion_group);
109 var that = this;
109 var that = this;
110 var accordion_toggle = $('<a />')
110 var accordion_toggle = $('<a />')
111 .addClass('accordion-toggle')
111 .addClass('accordion-toggle')
112 .attr('data-toggle', 'collapse')
112 .attr('data-toggle', 'collapse')
113 .attr('data-parent', '#' + this.$el.attr('id'))
113 .attr('data-parent', '#' + this.$el.attr('id'))
114 .attr('href', '#' + uuid)
114 .attr('href', '#' + uuid)
115 .click(function(evt){
115 .click(function(evt){
116
116
117 // Calling model.set will trigger all of the other views of the
117 // Calling model.set will trigger all of the other views of the
118 // model to update.
118 // model to update.
119 that.model.set("selected_index", index, {updated_view: that});
119 that.model.set("selected_index", index, {updated_view: that});
120 that.touch();
120 that.touch();
121 })
121 })
122 .text('Page ' + index)
122 .text('Page ' + index)
123 .appendTo(accordion_heading);
123 .appendTo(accordion_heading);
124 var accordion_body = $('<div />', {id: uuid})
124 var accordion_body = $('<div />', {id: uuid})
125 .addClass('panel-collapse collapse')
125 .addClass('panel-collapse collapse')
126 .appendTo(accordion_group);
126 .appendTo(accordion_group);
127 var accordion_inner = $('<div />')
127 var accordion_inner = $('<div />')
128 .addClass('panel-body')
128 .addClass('panel-body')
129 .appendTo(accordion_body);
129 .appendTo(accordion_body);
130 var container_index = this.containers.push(accordion_group) - 1;
130 var container_index = this.containers.push(accordion_group) - 1;
131 accordion_group.container_index = container_index;
131 accordion_group.container_index = container_index;
132 this.model_containers[model.id] = accordion_group;
132 this.model_containers[model.id] = accordion_group;
133 accordion_inner.append(view.$el);
133 accordion_inner.append(view.$el);
134
134
135 this.update();
135 this.update();
136 this.update_titles();
136 this.update_titles();
137
137
138 // Trigger the displayed event if this model is displayed.
138 // Trigger the displayed event if this model is displayed.
139 if (this.is_displayed) {
139 if (this.is_displayed) {
140 model.trigger('displayed');
140 model.trigger('displayed');
141 }
141 }
142 },
142 },
143 });
143 });
144 WidgetManager.register_widget_view('AccordionView', AccordionView);
144 WidgetManager.register_widget_view('AccordionView', AccordionView);
145
145
146
146
147 var TabView = IPython.DOMWidgetView.extend({
147 var TabView = IPython.DOMWidgetView.extend({
148 initialize: function() {
148 initialize: function() {
149 // Public constructor.
149 // Public constructor.
150 this.containers = [];
150 this.containers = [];
151 TabView.__super__.initialize.apply(this, arguments);
151 TabView.__super__.initialize.apply(this, arguments);
152 },
152 },
153
153
154 render: function(){
154 render: function(){
155 // Called when view is rendered.
155 // Called when view is rendered.
156 var uuid = 'tabs'+IPython.utils.uuid();
156 var uuid = 'tabs'+IPython.utils.uuid();
157 var that = this;
157 var that = this;
158 this.$tabs = $('<div />', {id: uuid})
158 this.$tabs = $('<div />', {id: uuid})
159 .addClass('nav')
159 .addClass('nav')
160 .addClass('nav-tabs')
160 .addClass('nav-tabs')
161 .appendTo(this.$el);
161 .appendTo(this.$el);
162 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
162 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
163 .addClass('tab-content')
163 .addClass('tab-content')
164 .appendTo(this.$el);
164 .appendTo(this.$el);
165 this.containers = [];
165 this.containers = [];
166 this.update_children([], this.model.get('_children'));
166 this.update_children([], this.model.get('children'));
167 this.model.on('change:_children', function(model, value, options) {
167 this.model.on('change:children', function(model, value, options) {
168 this.update_children(model.previous('_children'), value);
168 this.update_children(model.previous('children'), value);
169 }, this);
169 }, this);
170
170
171 // Trigger model displayed events for any models that are child to
171 // Trigger model displayed events for any models that are child to
172 // this model when this model is displayed.
172 // this model when this model is displayed.
173 this.model.on('displayed', function(){
173 this.model.on('displayed', function(){
174 that.is_displayed = true;
174 that.is_displayed = true;
175 for (var property in that.child_views) {
175 for (var property in that.child_views) {
176 if (that.child_views.hasOwnProperty(property)) {
176 if (that.child_views.hasOwnProperty(property)) {
177 that.child_views[property].model.trigger('displayed');
177 that.child_views[property].model.trigger('displayed');
178 }
178 }
179 }
179 }
180 });
180 });
181 },
181 },
182
182
183 update_children: function(old_list, new_list) {
183 update_children: function(old_list, new_list) {
184 // Called when the children list is modified.
184 // Called when the children list is modified.
185 this.do_diff(old_list,
185 this.do_diff(old_list,
186 new_list,
186 new_list,
187 $.proxy(this.remove_child_model, this),
187 $.proxy(this.remove_child_model, this),
188 $.proxy(this.add_child_model, this));
188 $.proxy(this.add_child_model, this));
189 },
189 },
190
190
191 remove_child_model: function(model) {
191 remove_child_model: function(model) {
192 // Called when a child is removed from children list.
192 // Called when a child is removed from children list.
193 var view = this.child_views[model.id];
193 var view = this.delete_child_view(model);
194 this.containers.splice(view.parent_tab.tab_text_index, 1);
194 this.containers.splice(view.parent_tab.tab_text_index, 1);
195 view.parent_tab.remove();
195 view.parent_tab.remove();
196 view.parent_container.remove();
196 view.parent_container.remove();
197 view.remove();
197 view.remove();
198 this.delete_child_view(model);
199 },
198 },
200
199
201 add_child_model: function(model) {
200 add_child_model: function(model) {
202 // Called when a child is added to children list.
201 // Called when a child is added to children list.
203 var view = this.create_child_view(model);
202 var view = this.create_child_view(model);
204 var index = this.containers.length;
203 var index = this.containers.length;
205 var uuid = IPython.utils.uuid();
204 var uuid = IPython.utils.uuid();
206
205
207 var that = this;
206 var that = this;
208 var tab = $('<li />')
207 var tab = $('<li />')
209 .css('list-style-type', 'none')
208 .css('list-style-type', 'none')
210 .appendTo(this.$tabs);
209 .appendTo(this.$tabs);
211 view.parent_tab = tab;
210 view.parent_tab = tab;
212
211
213 var tab_text = $('<a />')
212 var tab_text = $('<a />')
214 .attr('href', '#' + uuid)
213 .attr('href', '#' + uuid)
215 .attr('data-toggle', 'tab')
214 .attr('data-toggle', 'tab')
216 .text('Page ' + index)
215 .text('Page ' + index)
217 .appendTo(tab)
216 .appendTo(tab)
218 .click(function (e) {
217 .click(function (e) {
219
218
220 // Calling model.set will trigger all of the other views of the
219 // Calling model.set will trigger all of the other views of the
221 // model to update.
220 // model to update.
222 that.model.set("selected_index", index, {updated_view: this});
221 that.model.set("selected_index", index, {updated_view: this});
223 that.touch();
222 that.touch();
224 that.select_page(index);
223 that.select_page(index);
225 });
224 });
226 tab.tab_text_index = this.containers.push(tab_text) - 1;
225 tab.tab_text_index = this.containers.push(tab_text) - 1;
227
226
228 var contents_div = $('<div />', {id: uuid})
227 var contents_div = $('<div />', {id: uuid})
229 .addClass('tab-pane')
228 .addClass('tab-pane')
230 .addClass('fade')
229 .addClass('fade')
231 .append(view.$el)
230 .append(view.$el)
232 .appendTo(this.$tab_contents);
231 .appendTo(this.$tab_contents);
233 view.parent_container = contents_div;
232 view.parent_container = contents_div;
234
233
235 // Trigger the displayed event if this model is displayed.
234 // Trigger the displayed event if this model is displayed.
236 if (this.is_displayed) {
235 if (this.is_displayed) {
237 model.trigger('displayed');
236 model.trigger('displayed');
238 }
237 }
239 },
238 },
240
239
241 update: function(options) {
240 update: function(options) {
242 // Update the contents of this view
241 // Update the contents of this view
243 //
242 //
244 // Called when the model is changed. The model may have been
243 // Called when the model is changed. The model may have been
245 // changed by another view or by a state update from the back-end.
244 // changed by another view or by a state update from the back-end.
246 if (options === undefined || options.updated_view != this) {
245 if (options === undefined || options.updated_view != this) {
247 // Set tab titles
246 // Set tab titles
248 var titles = this.model.get('_titles');
247 var titles = this.model.get('_titles');
249 var that = this;
248 var that = this;
250 _.each(titles, function(title, page_index) {
249 _.each(titles, function(title, page_index) {
251 var tab_text = that.containers[page_index];
250 var tab_text = that.containers[page_index];
252 if (tab_text !== undefined) {
251 if (tab_text !== undefined) {
253 tab_text.text(title);
252 tab_text.text(title);
254 }
253 }
255 });
254 });
256
255
257 var selected_index = this.model.get('selected_index');
256 var selected_index = this.model.get('selected_index');
258 if (0 <= selected_index && selected_index < this.containers.length) {
257 if (0 <= selected_index && selected_index < this.containers.length) {
259 this.select_page(selected_index);
258 this.select_page(selected_index);
260 }
259 }
261 }
260 }
262 return TabView.__super__.update.apply(this);
261 return TabView.__super__.update.apply(this);
263 },
262 },
264
263
265 select_page: function(index) {
264 select_page: function(index) {
266 // Select a page.
265 // Select a page.
267 this.$tabs.find('li')
266 this.$tabs.find('li')
268 .removeClass('active');
267 .removeClass('active');
269 this.containers[index].tab('show');
268 this.containers[index].tab('show');
270 },
269 },
271 });
270 });
272 WidgetManager.register_widget_view('TabView', TabView);
271 WidgetManager.register_widget_view('TabView', TabView);
273 });
272 });
@@ -1,35 +1,33
1 """ContainerWidget class.
1 """ContainerWidget class.
2
2
3 Represents a container that can be used to group other widgets.
3 Represents a container that can be used to group other widgets.
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 from .widget import DOMWidget
9 from .widget import DOMWidget
10 from IPython.utils.traitlets import Unicode, Tuple, TraitError
10 from IPython.utils.traitlets import Unicode, Tuple, TraitError
11
11
12 class ContainerWidget(DOMWidget):
12 class ContainerWidget(DOMWidget):
13 _view_name = Unicode('ContainerView', sync=True)
13 _view_name = Unicode('ContainerView', sync=True)
14
14
15 # Child widgets in the container.
15 # Child widgets in the container.
16 # Using a tuple here to force reassignment to update the list.
16 # Using a tuple here to force reassignment to update the list.
17 # When a proper notifying-list trait exists, that is what should be used here.
17 # When a proper notifying-list trait exists, that is what should be used here.
18 children = Tuple()
18 children = Tuple(sync=True)
19 _children = Tuple(sync=True)
20
21
19
22 def __init__(self, **kwargs):
20 def __init__(self, **kwargs):
23 super(ContainerWidget, self).__init__(**kwargs)
21 super(ContainerWidget, self).__init__(**kwargs)
24 self.on_displayed(ContainerWidget._fire_children_displayed)
22 self.on_displayed(ContainerWidget._fire_children_displayed)
25
23
26 def _fire_children_displayed(self):
24 def _fire_children_displayed(self):
27 for child in self._children:
25 for child in self.children:
28 child._handle_displayed()
26 child._handle_displayed()
29
27
30
28
31 class PopupWidget(ContainerWidget):
29 class PopupWidget(ContainerWidget):
32 _view_name = Unicode('PopupView', sync=True)
30 _view_name = Unicode('PopupView', sync=True)
33
31
34 description = Unicode(sync=True)
32 description = Unicode(sync=True)
35 button_text = Unicode(sync=True)
33 button_text = Unicode(sync=True)
General Comments 0
You need to be logged in to leave comments. Login now