##// END OF EJS Templates
Got containers and mutlicontainers working! Yay
Jonathan Frederic -
Show More
@@ -1,379 +1,366
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(["notebook/js/widgetmanager",
18 18 "underscore",
19 19 "backbone"],
20 20 function(widget_manager, underscore, backbone){
21 21
22 22 //--------------------------------------------------------------------
23 23 // WidgetModel class
24 24 //--------------------------------------------------------------------
25 25 var WidgetModel = Backbone.Model.extend({
26 26 constructor: function (widget_manager, model_id, comm) {
27 27 // Construcctor
28 28 //
29 29 // Creates a WidgetModel instance.
30 30 //
31 31 // Parameters
32 32 // ----------
33 33 // widget_manager : WidgetManager instance
34 34 // model_id : string
35 35 // An ID unique to this model.
36 36 // comm : Comm instance (optional)
37 37 this.widget_manager = widget_manager;
38 38 this.pending_msgs = 0;
39 39 this.msg_throttle = 2;
40 40 this.msg_buffer = null;
41 41 this.key_value_lock = null;
42 42 this.id = model_id;
43 43 this.views = [];
44 44
45 45 if (comm !== undefined) {
46 46 // Remember comm associated with the model.
47 47 this.comm = comm;
48 48 comm.model = this;
49 49
50 50 // Hook comm messages up to model.
51 51 comm.on_close($.proxy(this._handle_comm_closed, this));
52 52 comm.on_msg($.proxy(this._handle_comm_msg, this));
53 53 }
54 54 return Backbone.Model.apply(this);
55 55 },
56 56
57 57 send: function (content, callbacks) {
58 58 if (this.comm !== undefined) {
59 59 var data = {method: 'custom', custom_content: content};
60 60 this.comm.send(data, callbacks);
61 61 }
62 62 },
63 63
64 64 // Handle when a widget is closed.
65 65 _handle_comm_closed: function (msg) {
66 66 this.trigger('comm:close');
67 67 delete this.comm.model; // Delete ref so GC will collect widget model.
68 68 delete this.comm;
69 69 delete this.model_id; // Delete id from model so widget manager cleans up.
70 70 // TODO: Handle deletion, like this.destroy(), and delete views, etc.
71 71 },
72 72
73 73
74 74 // Handle incoming comm msg.
75 75 _handle_comm_msg: function (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.custom_content);
83 83 break;
84 84 case 'display':
85 85 this.widget_manager.display_view(msg.parent_header.msg_id, this);
86 86 break;
87 87 }
88 88 },
89 89
90 90
91 91 // Handle when a widget is updated via the python side.
92 92 apply_update: function (state) {
93 93 for (var key in state) {
94 94 if (state.hasOwnProperty(key)) {
95 95 var value = state[key];
96 96 this.key_value_lock = [key, value];
97 97 try {
98 98 this.set(key, this._unpack_models(value));
99 99 } finally {
100 100 this.key_value_lock = null;
101 101 }
102 102 }
103 103 }
104 104 //TODO: are there callbacks that make sense in this case? If so, attach them here as an option
105 105 this.save();
106 106 },
107 107
108 108
109 109 _handle_status: function (msg, callbacks) {
110 110 //execution_state : ('busy', 'idle', 'starting')
111 111 if (this.comm !== undefined) {
112 112 if (msg.content.execution_state ==='idle') {
113 113 // Send buffer if this message caused another message to be
114 114 // throttled.
115 115 if (this.msg_buffer !== null &&
116 116 this.msg_throttle === this.pending_msgs) {
117 117 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
118 118 this.comm.send(data, callbacks);
119 119 this.msg_buffer = null;
120 120 } else {
121 121 --this.pending_msgs;
122 122 }
123 123 }
124 124 }
125 125 },
126 126
127 127
128 128 // Custom syncronization logic.
129 129 _handle_sync: function (method, options) {
130 130 var model_json = this.toJSON();
131 131 var attr;
132 132
133 133 // Only send updated state if the state hasn't been changed
134 134 // during an update.
135 135 if (this.comm !== undefined) {
136 136 if (this.pending_msgs >= this.msg_throttle) {
137 137 // The throttle has been exceeded, buffer the current msg so
138 138 // it can be sent once the kernel has finished processing
139 139 // some of the existing messages.
140 140 if (this.msg_buffer === null) {
141 141 this.msg_buffer = $.extend({}, model_json); // Copy
142 142 }
143 143 for (attr in options.attrs) {
144 144 var value = this._pack_models(options.attrs[attr]);
145 145 if (this.key_value_lock === null || attr !== this.key_value_lock[0] || value !== this.key_value_lock[1]) {
146 146 this.msg_buffer[attr] = value;
147 147 }
148 148 }
149 149
150 150 } else {
151 151 // We haven't exceeded the throttle, send the message like
152 152 // normal. If this is a patch operation, just send the
153 153 // changes.
154 154 var send_json = model_json;
155 155 send_json = {};
156 156 for (attr in options.attrs) {
157 157 var value = this._pack_models(options.attrs[attr]);
158 158 if (this.key_value_lock === null || attr !== this.key_value_lock[0] || value !== this.key_value_lock[1]) {
159 159 send_json[attr] = value;
160 160 }
161 161 }
162 162
163 163 var is_empty = true;
164 164 for (var prop in send_json) if (send_json.hasOwnProperty(prop)) is_empty = false;
165 165 if (!is_empty) {
166 166 ++this.pending_msgs;
167 167 var data = {method: 'backbone', sync_data: send_json};
168 168 this.comm.send(data, options.callbacks);
169 169 }
170 170 }
171 171 }
172 172
173 173 // Since the comm is a one-way communication, assume the message
174 174 // arrived.
175 175 return model_json;
176 176 },
177 177
178 178 _pack_models: function(value) {
179 179 if (value instanceof Backbone.Model) {
180 180 return value.id;
181 181 } else if (value instanceof Object) {
182 182 var packed = {};
183 183 for (var key in value) {
184 184 packed[key] = this._pack_models(value[key]);
185 185 }
186 186 return packed;
187 187 } else {
188 188 return value;
189 189 }
190 190 },
191 191
192 192 _unpack_models: function(value) {
193 193 if (value instanceof Object) {
194 194 var unpacked = {};
195 195 for (var key in value) {
196 196 unpacked[key] = this._unpack_models(value[key]);
197 197 }
198 198 return unpacked;
199 199 } else {
200 200 var model = this.widget_manager.get_model(value);
201 201 if (model !== null) {
202 202 return model;
203 203 } else {
204 204 return value;
205 205 }
206 206 }
207 207 },
208 208
209 209 });
210 210 widget_manager.register_widget_model('WidgetModel', WidgetModel);
211 211
212 212
213 213 //--------------------------------------------------------------------
214 214 // WidgetView class
215 215 //--------------------------------------------------------------------
216 216 var WidgetView = Backbone.View.extend({
217 217 initialize: function(parameters) {
218 218 this.model.on('change',this.update,this);
219 219 this.options = parameters.options;
220 220 this.child_views = [];
221 221 this.model.views.push(this);
222 222 },
223 223
224 224 update: function(){
225 225 // update view to be consistent with this.model
226 226 // triggered on model change
227 227 },
228 228
229 child_view: function(child_model, options) {
229 create_child_view: function(child_model, options) {
230 230 // create and return a child view, given a model and (optionally) a view name
231 231 // if the view name is not given, it defaults to the model's default view attribute
232 232 var child_view = this.model.widget_manager.create_view(child_model, options);
233 233 this.child_views[child_model.id] = child_view;
234 234 return child_view;
235 235 },
236 236
237 update_child_views: function(old_list, new_list) {
238 // this function takes an old list and new list of models
239 // views in child_views that correspond to deleted ids are deleted
240 // views corresponding to added ids are added child_views
241
242 // delete old views
243 _.each(_.difference(old_list, new_list), function(element, index, list) {
244 var view = this.child_views[element.id];
245 delete this.child_views[element.id];
237 delete_child_view: function(child_model, options) {
238 var view = this.child_views[child_model.id];
239 delete this.child_views[child_model.id];
246 240 view.remove();
247 }, this);
248
249 // add new views
250 _.each(_.difference(new_list, old_list), function(element, index, list) {
251 // this function adds the view to the child_views dictionary
252 this.child_view(element);
253 }, this);
254 241 },
255 242
256 243 do_diff: function(old_list, new_list, removed_callback, added_callback) {
257 244 // Difference a changed list and call remove and add callbacks for
258 245 // each removed and added item in the new list.
259 246 //
260 247 // Parameters
261 248 // ----------
262 249 // old_list : array
263 250 // new_list : array
264 251 // removed_callback : Callback(item)
265 252 // Callback that is called for each item removed.
266 253 // added_callback : Callback(item)
267 254 // Callback that is called for each item added.
268 255
269 256
270 257 // removed items
271 258 _.each(_.difference(old_list, new_list), function(item, index, list) {
272 259 removed_callback(item);
273 260 }, this);
274 261
275 262 // added items
276 263 _.each(_.difference(new_list, old_list), function(item, index, list) {
277 264 added_callback(item);
278 265 }, this);
279 266 },
280 267
281 268 callbacks: function(){
282 269 return this.model.widget_manager.callbacks(this);
283 270 },
284 271
285 272 render: function(){
286 273 // render the view. By default, this is only called the first time the view is created
287 274 },
288 275 send: function (content) {
289 276 this.model.send(content, this.callbacks());
290 277 },
291 278
292 279 touch: function () {
293 280 this.model.save(this.model.changedAttributes(), {patch: true, callbacks: this.callbacks()});
294 281 },
295 282
296 283 });
297 284
298 285 var DOMWidgetView = WidgetView.extend({
299 286 initialize: function (options) {
300 287 // TODO: make changes more granular (e.g., trigger on visible:change)
301 288 this.model.on('change', this.update, this);
302 289 this.model.on('msg:custom', this.on_msg, this);
303 290 DOMWidgetView.__super__.initialize.apply(this, arguments);
304 291 },
305 292
306 293 on_msg: function(msg) {
307 294 switch(msg.msg_type) {
308 295 case 'add_class':
309 296 this.add_class(msg.selector, msg.class_list);
310 297 break;
311 298 case 'remove_class':
312 299 this.remove_class(msg.selector, msg.class_list);
313 300 break;
314 301 }
315 302 },
316 303
317 304 add_class: function (selector, class_list) {
318 305 this._get_selector_element(selector).addClass(class_list);
319 306 },
320 307
321 308 remove_class: function (selector, class_list) {
322 309 this._get_selector_element(selector).removeClass(class_list);
323 310 },
324 311
325 312 update: function () {
326 313 // Update the contents of this view
327 314 //
328 315 // Called when the model is changed. The model may have been
329 316 // changed by another view or by a state update from the back-end.
330 317 // The very first update seems to happen before the element is
331 318 // finished rendering so we use setTimeout to give the element time
332 319 // to render
333 320 var e = this.$el;
334 321 var visible = this.model.get('visible');
335 322 setTimeout(function() {e.toggle(visible)},0);
336 323
337 324 var css = this.model.get('_css');
338 325 if (css === undefined) {return;}
339 326 for (var selector in css) {
340 327 if (css.hasOwnProperty(selector)) {
341 328 // Apply the css traits to all elements that match the selector.
342 329 var elements = this._get_selector_element(selector);
343 330 if (elements.length > 0) {
344 331 var css_traits = css[selector];
345 332 for (var css_key in css_traits) {
346 333 if (css_traits.hasOwnProperty(css_key)) {
347 334 elements.css(css_key, css_traits[css_key]);
348 335 }
349 336 }
350 337 }
351 338 }
352 339 }
353 340 },
354 341
355 342 _get_selector_element: function (selector) {
356 343 // Get the elements via the css selector. If the selector is
357 344 // blank, apply the style to the $el_to_style element. If
358 345 // the $el_to_style element is not defined, use apply the
359 346 // style to the view's element.
360 347 var elements;
361 348 if (selector === undefined || selector === null || selector === '') {
362 349 if (this.$el_to_style === undefined) {
363 350 elements = this.$el;
364 351 } else {
365 352 elements = this.$el_to_style;
366 353 }
367 354 } else {
368 355 elements = this.$el.find(selector);
369 356 }
370 357 return elements;
371 358 },
372 359 });
373 360
374 361 IPython.WidgetModel = WidgetModel;
375 362 IPython.WidgetView = WidgetView;
376 363 IPython.DOMWidgetView = DOMWidgetView;
377 364
378 365 return widget_manager;
379 366 });
@@ -1,252 +1,270
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(["notebook/js/widgets/widget"], function(widget_manager) {
18 18 var ContainerView = IPython.DOMWidgetView.extend({
19 19
20 20 render: function(){
21 21 this.$el
22 22 .addClass('widget-container');
23 23 this.children={};
24 24 this.update_children([], this.model.get('children'));
25 25 this.model.on('change:children', function(model, value, options) {
26 26 this.update_children(model.previous('children'), value);
27 27 }, this);
28 28 this.update()
29 29 },
30 30
31 31 update_children: function(old_list, new_list) {
32 this.$el.empty();
33 this.update_child_views(old_list, new_list);
34 _.each(new_list, function(element, index, list) {
35 this.$el.append(this.child_views[element.id].$el);
36 }, this)
32 this.do_diff(old_list,
33 new_list,
34 $.proxy(this.remove_child_model, this),
35 $.proxy(this.add_child_model, this));
36 },
37
38 remove_child_model: function(model) {
39 this.child_views[model.id].remove();
40 this.delete_child_view(model);
41 },
42
43 add_child_model: function(model) {
44 var view = this.create_child_view(model);
45 this.$el.append(view.$el);
37 46 },
38 47
39 48 update: function(){
40 49 // Update the contents of this view
41 50 //
42 51 // Called when the model is changed. The model may have been
43 52 // changed by another view or by a state update from the back-end.
44 53 return ContainerView.__super__.update.apply(this);
45 54 },
46 55 });
47 56
48 57 widget_manager.register_widget_view('ContainerView', ContainerView);
49 58
50 59
51 60 var ModalView = IPython.DOMWidgetView.extend({
52 61
53 62 render: function(){
54 63 var that = this;
55 64 this.children={};
56 65 this.update_children([], this.model.get('children'));
57 66 this.model.on('change:children', function(model, value, options) {
58 67 this.update_children(model.previous('children'), value);
59 68 }, this);
60 69
61 70 this.$el
62 71 .html('')
63 72 .on("remove", function(){
64 73 that.$window.remove();
65 74 });
66 75 this.$window = $('<div />')
67 76 .addClass('modal widget-modal')
68 77 .appendTo($('#notebook-container'))
69 78 .mousedown(function(){
70 79 that.bring_to_front();
71 80 });
72 81 this.$title_bar = $('<div />')
73 82 .addClass('popover-title')
74 83 .appendTo(this.$window)
75 84 .mousedown(function(){
76 85 that.bring_to_front();
77 86 });
78 87 this.$close = $('<button />')
79 88 .addClass('close icon-remove')
80 89 .css('margin-left', '5px')
81 90 .appendTo(this.$title_bar)
82 91 .click(function(){
83 92 that.hide();
84 93 event.stopPropagation();
85 94 });
86 95 this.$minimize = $('<button />')
87 96 .addClass('close icon-arrow-down')
88 97 .appendTo(this.$title_bar)
89 98 .click(function(){
90 99 that.popped_out = !that.popped_out;
91 100 if (!that.popped_out) {
92 101 that.$minimize
93 102 .removeClass('icon-arrow-down')
94 103 .addClass('icon-arrow-up');
95 104
96 105 that.$window
97 106 .draggable('destroy')
98 107 .resizable('destroy')
99 108 .removeClass('widget-modal modal')
100 109 .addClass('docked-widget-modal')
101 110 .detach()
102 111 .insertBefore(that.$show_button);
103 112 that.$show_button.hide();
104 113 that.$close.hide();
105 114 } else {
106 115 that.$minimize
107 116 .addClass('icon-arrow-down')
108 117 .removeClass('icon-arrow-up');
109 118
110 119 that.$window
111 120 .removeClass('docked-widget-modal')
112 121 .addClass('widget-modal modal')
113 122 .detach()
114 123 .appendTo($('#notebook-container'))
115 124 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
116 125 .resizable()
117 126 .children('.ui-resizable-handle').show();
118 127 that.show();
119 128 that.$show_button.show();
120 129 that.$close.show();
121 130 }
122 131 event.stopPropagation();
123 132 });
124 133 this.$title = $('<div />')
125 134 .addClass('widget-modal-title')
126 135 .html('&nbsp;')
127 136 .appendTo(this.$title_bar);
128 137 this.$body = $('<div />')
129 138 .addClass('modal-body')
130 139 .addClass('widget-modal-body')
131 140 .addClass('widget-container')
132 141 .appendTo(this.$window);
133 142
134 143 this.$show_button = $('<button />')
135 144 .html('&nbsp;')
136 145 .addClass('btn btn-info widget-modal-show')
137 146 .appendTo(this.$el)
138 147 .click(function(){
139 148 that.show();
140 149 });
141 150
142 151 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
143 152 this.$window.resizable();
144 153 this.$window.on('resize', function(){
145 154 that.$body.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
146 155 });
147 156
148 157 this.$el_to_style = this.$body;
149 158 this._shown_once = false;
150 159 this.popped_out = true;
151 160 },
152 161
153 162 hide: function() {
154 163 this.$window.hide();
155 164 this.$show_button.removeClass('btn-info');
156 165 },
157 166
158 167 show: function() {
159 168 this.$show_button.addClass('btn-info');
160 169
161 170 this.$window.show();
162 171 if (this.popped_out) {
163 172 this.$window.css("positon", "absolute");
164 173 this.$window.css("top", "0px");
165 174 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
166 175 $(window).scrollLeft()) + "px");
167 176 this.bring_to_front();
168 177 }
169 178 },
170 179
171 180 bring_to_front: function() {
172 181 var $widget_modals = $(".widget-modal");
173 182 var max_zindex = 0;
174 183 $widget_modals.each(function (index, el){
175 184 max_zindex = Math.max(max_zindex, parseInt($(el).css('z-index')));
176 185 });
177 186
178 187 // Start z-index of widget modals at 2000
179 188 max_zindex = Math.max(max_zindex, 2000);
180 189
181 190 $widget_modals.each(function (index, el){
182 191 $el = $(el);
183 192 if (max_zindex == parseInt($el.css('z-index'))) {
184 193 $el.css('z-index', max_zindex - 1);
185 194 }
186 195 });
187 196 this.$window.css('z-index', max_zindex);
188 197 },
189 198
190 199 update_children: function(old_list, new_list) {
191 this.$el.empty();
192 this.update_child_views(old_list, new_list);
193 _.each(new_list, function(element, index, list) {
194 this.$body.append(this.child_views[element].$el);
195 }, this)
200 this.do_diff(old_list,
201 new_list,
202 $.proxy(this.remove_child_model, this),
203 $.proxy(this.add_child_model, this));
204 },
205
206 remove_child_model: function(model) {
207 this.child_views[model.id].remove();
208 this.delete_child_view(model);
209 },
210
211 add_child_model: function(model) {
212 var view = this.create_child_view(model);
213 this.$body.append(view.$el);
196 214 },
197 215
198 216 update: function(){
199 217 // Update the contents of this view
200 218 //
201 219 // Called when the model is changed. The model may have been
202 220 // changed by another view or by a state update from the back-end.
203 221 var description = this.model.get('description');
204 222 description = description.replace(/ /g, '&nbsp;', 'm');
205 223 description = description.replace(/\n/g, '<br>\n', 'm');
206 224 if (description.length === 0) {
207 225 this.$title.html('&nbsp;'); // Preserve title height
208 226 } else {
209 227 this.$title.html(description);
210 228 }
211 229
212 230 var button_text = this.model.get('button_text');
213 231 button_text = button_text.replace(/ /g, '&nbsp;', 'm');
214 232 button_text = button_text.replace(/\n/g, '<br>\n', 'm');
215 233 if (button_text.length === 0) {
216 234 this.$show_button.html('&nbsp;'); // Preserve button height
217 235 } else {
218 236 this.$show_button.html(button_text);
219 237 }
220 238
221 239 if (!this._shown_once) {
222 240 this._shown_once = true;
223 241 this.show();
224 242 }
225 243
226 244 return ModalView.__super__.update.apply(this);
227 245 },
228 246
229 247 _get_selector_element: function(selector) {
230 248
231 249 // Since the modal actually isn't within the $el in the DOM, we need to extend
232 250 // the selector logic to allow the user to set css on the modal if need be.
233 251 // The convention used is:
234 252 // "modal" - select the modal div
235 253 // "modal [selector]" - select element(s) within the modal div.
236 254 // "[selector]" - select elements within $el
237 255 // "" - select the $el_to_style
238 256 if (selector.substring(0, 5) == 'modal') {
239 257 if (selector == 'modal') {
240 258 return this.$window;
241 259 } else {
242 260 return this.$window.find(selector.substring(6));
243 261 }
244 262 } else {
245 263 return ModalView.__super__._get_selector_element.apply(this, [selector]);
246 264 }
247 265 },
248 266
249 267 });
250 268
251 269 widget_manager.register_widget_view('ModalView', ModalView);
252 270 });
@@ -1,222 +1,248
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(["notebook/js/widgets/widget"], function(widget_manager){
18 18 var AccordionView = IPython.DOMWidgetView.extend({
19 19
20 20 render: function(){
21 21 var guid = 'accordion' + IPython.utils.uuid();
22 22 this.$el
23 23 .attr('id', guid)
24 24 .addClass('accordion');
25 25 this.containers = [];
26 this.model_containers = {};
26 27 this.update_children([], this.model.get('children'));
27 28 this.model.on('change:children', function(model, value, options) {
28 29 this.update_children(model.previous('children'), value);
29 30 }, this);
30 31 },
31 32
32 update_children: function(old_list, new_list) {
33 _.each(this.containers, function(element, index, list) {
34 element.remove();
35 }, this);
36 this.containers = [];
37 this.update_child_views(old_list, new_list);
38 _.each(new_list, function(element, index, list) {
39 this.add_child_view(this.child_views[element]);
40 }, this)
41 },
42
43
44 33 update: function(options) {
45 34 // Update the contents of this view
46 35 //
47 36 // Called when the model is changed. The model may have been
48 37 // changed by another view or by a state update from the back-end.
49 38
50 39 if (options === undefined || options.updated_view != this) {
51 40 // Set tab titles
52 41 var titles = this.model.get('_titles');
53 42 for (var page_index in titles) {
54 43
55 44 var accordian = this.containers[page_index];
56 45 if (accordian !== undefined) {
57 46 accordian
58 47 .find('.accordion-heading')
59 48 .find('.accordion-toggle')
60 49 .html(titles[page_index]);
61 50 }
62 51 }
63 52
64 53 // Set selected page
65 54 var selected_index = this.model.get("selected_index");
66 55 if (0 <= selected_index && selected_index < this.containers.length) {
67 56 for (var index in this.containers) {
68 57 if (index==selected_index) {
69 58 this.containers[index].find('.accordion-body').collapse('show');
70 59 } else {
71 60 this.containers[index].find('.accordion-body').collapse('hide');
72 61 }
73 62
74 63 }
75 64 }
76 65 }
77 66 return AccordionView.__super__.update.apply(this);
78 67 },
79 68
80 add_child_view: function(view) {
69 update_children: function(old_list, new_list) {
70 this.do_diff(old_list,
71 new_list,
72 $.proxy(this.remove_child_model, this),
73 $.proxy(this.add_child_model, this));
74 },
75
76 remove_child_model: function(model) {
77 var accordion_group = this.model_containers[model.id];
78 this.containers.splice(accordion_group.container_index, 1);
79 delete this.model_containers[model.id];
80 accordion_group.remove();
81 this.delete_child_view(model);
82 },
81 83
84 add_child_model: function(model) {
85 var view = this.create_child_view(model);
82 86 var index = this.containers.length;
83 87 var uuid = IPython.utils.uuid();
84 88 var accordion_group = $('<div />')
85 89 .addClass('accordion-group')
86 90 .appendTo(this.$el);
87 91 var accordion_heading = $('<div />')
88 92 .addClass('accordion-heading')
89 93 .appendTo(accordion_group);
90 94 var that = this;
91 95 var accordion_toggle = $('<a />')
92 96 .addClass('accordion-toggle')
93 97 .attr('data-toggle', 'collapse')
94 98 .attr('data-parent', '#' + this.$el.attr('id'))
95 99 .attr('href', '#' + uuid)
96 100 .click(function(evt){
97 101
98 102 // Calling model.set will trigger all of the other views of the
99 103 // model to update.
100 104 that.model.set("selected_index", index, {updated_view: this});
101 105 that.touch();
102 106 })
103 107 .html('Page ' + index)
104 108 .appendTo(accordion_heading);
105 109 var accordion_body = $('<div />', {id: uuid})
106 110 .addClass('accordion-body collapse')
107 111 .appendTo(accordion_group);
108 112 var accordion_inner = $('<div />')
109 113 .addClass('accordion-inner')
110 114 .appendTo(accordion_body);
111 this.containers.push(accordion_group);
115 var container_index = this.containers.push(accordion_group) - 1;
116 accordion_group.container_index = container_index;
117 this.model_containers[model.id] = accordion_group;
112 118 accordion_inner.append(view.$el);
113 119
114 120 this.update();
115 121
116 122 // Stupid workaround to close the bootstrap accordion tabs which
117 123 // open by default even though they don't have the `in` class
118 124 // attached to them. For some reason a delay is required.
119 125 // TODO: Better fix.
120 126 setTimeout(function(){ that.update(); }, 500);
121 127 },
122 128 });
123 129
124 130 widget_manager.register_widget_view('AccordionView', AccordionView);
125 131
126 132 var TabView = IPython.DOMWidgetView.extend({
127 133
128 134 initialize: function() {
129 135 this.containers = [];
130 136 TabView.__super__.initialize.apply(this, arguments);
131 137 },
132 138
133 139 render: function(){
134 140 var uuid = 'tabs'+IPython.utils.uuid();
135 141 var that = this;
136 142 this.$tabs = $('<div />', {id: uuid})
137 143 .addClass('nav')
138 144 .addClass('nav-tabs')
139 145 .appendTo(this.$el);
140 146 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
141 147 .addClass('tab-content')
142 148 .appendTo(this.$el);
143 149 this.containers = [];
144 150 this.update_children([], this.model.get('children'));
145 151 this.model.on('change:children', function(model, value, options) {
146 152 this.update_children(model.previous('children'), value);
147 153 }, this);
148 154 },
149 155
150 156 update_children: function(old_list, new_list) {
151 157 _.each(this.containers, function(element, index, list) {
152 158 element.remove();
153 159 }, this);
154 160 this.containers = [];
155 161 this.update_child_views(old_list, new_list);
156 162 _.each(new_list, function(element, index, list) {
157 163 this.add_child_view(this.child_views[element]);
158 164 }, this)
159 165 },
160 166
161 update: function(options) {
162 // Update the contents of this view
163 //
164 // Called when the model is changed. The model may have been
165 // changed by another view or by a state update from the back-end.
166 if (options === undefined || options.updated_view != this) {
167 // Set tab titles
168 var titles = this.model.get('_titles');
169 for (var page_index in titles) {
170 var tab_text = this.containers[page_index];
171 if (tab_text !== undefined) {
172 tab_text.html(titles[page_index]);
173 }
174 }
167 update_children: function(old_list, new_list) {
168 this.do_diff(old_list,
169 new_list,
170 $.proxy(this.remove_child_model, this),
171 $.proxy(this.add_child_model, this));
172 },
175 173
176 var selected_index = this.model.get('selected_index');
177 if (0 <= selected_index && selected_index < this.containers.length) {
178 this.select_page(selected_index);
179 }
180 }
181 return TabView.__super__.update.apply(this);
174 remove_child_model: function(model) {
175 var view = this.child_views[model.id];
176 this.containers.splice(view.parent_tab.tab_text_index, 1);
177 view.parent_tab.remove();
178 view.parent_container.remove();
179 view.remove();
180 this.delete_child_view(model);
182 181 },
183 182
184 add_child_view: function(view) {
183 add_child_model: function(model) {
184 var view = this.create_child_view(model);
185 185 var index = this.containers.length;
186 186 var uuid = IPython.utils.uuid();
187 187
188 188 var that = this;
189 189 var tab = $('<li />')
190 190 .css('list-style-type', 'none')
191 191 .appendTo(this.$tabs);
192 view.parent_tab = tab;
193
192 194 var tab_text = $('<a />')
193 195 .attr('href', '#' + uuid)
194 196 .attr('data-toggle', 'tab')
195 197 .html('Page ' + index)
196 198 .appendTo(tab)
197 199 .click(function (e) {
198 200
199 201 // Calling model.set will trigger all of the other views of the
200 202 // model to update.
201 203 that.model.set("selected_index", index, {updated_view: this});
202 204 that.touch();
203 205 that.select_page(index);
204 206 });
205 this.containers.push(tab_text);
207 tab.tab_text_index = this.containers.push(tab_text) - 1;
206 208
207 209 var contents_div = $('<div />', {id: uuid})
208 210 .addClass('tab-pane')
209 211 .addClass('fade')
210 212 .append(view.$el)
211 213 .appendTo(this.$tab_contents);
214 view.parent_container = contents_div;
215 },
216
217 update: function(options) {
218 // Update the contents of this view
219 //
220 // Called when the model is changed. The model may have been
221 // changed by another view or by a state update from the back-end.
222 if (options === undefined || options.updated_view != this) {
223 // Set tab titles
224 var titles = this.model.get('_titles');
225 for (var page_index in titles) {
226 var tab_text = this.containers[page_index];
227 if (tab_text !== undefined) {
228 tab_text.html(titles[page_index]);
229 }
230 }
231
232 var selected_index = this.model.get('selected_index');
233 if (0 <= selected_index && selected_index < this.containers.length) {
234 this.select_page(selected_index);
235 }
236 }
237 return TabView.__super__.update.apply(this);
212 238 },
213 239
214 240 select_page: function(index) {
215 241 this.$tabs.find('li')
216 242 .removeClass('active');
217 243 this.containers[index].tab('show');
218 244 },
219 245 });
220 246
221 247 widget_manager.register_widget_view('TabView', TabView);
222 248 });
@@ -1,60 +1,60
1 1 """SelectionContainerWidget class.
2 2
3 3 Represents a multipage container that can be used to group other widgets into
4 4 pages.
5 5 """
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (c) 2013, the IPython Development Team.
8 8 #
9 9 # Distributed under the terms of the Modified BSD License.
10 10 #
11 11 # The full license is in the file COPYING.txt, distributed with this software.
12 12 #-----------------------------------------------------------------------------
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Imports
16 16 #-----------------------------------------------------------------------------
17 17 from .widget import DOMWidget
18 18 from IPython.utils.traitlets import Unicode, Dict, Int, List, Instance
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Classes
22 22 #-----------------------------------------------------------------------------
23 23 class AccordionWidget(DOMWidget):
24 24 view_name = Unicode('AccordionView', sync=True)
25 25
26 26 # Keys
27 27 _titles = Dict(help="Titles of the pages", sync=True)
28 28 selected_index = Int(0, sync=True)
29 29
30 children = List(Instance(DOMWidget))
30 children = List(Instance(DOMWidget), sync=True)
31 31
32 32 # Public methods
33 33 def set_title(self, index, title):
34 34 """Sets the title of a container page
35 35
36 36 Parameters
37 37 ----------
38 38 index : int
39 39 Index of the container page
40 40 title : unicode
41 41 New title"""
42 42 self._titles[index] = title
43 43 self.send_state('_titles')
44 44
45 45
46 46 def get_title(self, index):
47 47 """Gets the title of a container pages
48 48
49 49 Parameters
50 50 ----------
51 51 index : int
52 52 Index of the container page"""
53 53 if index in self._titles:
54 54 return self._titles[index]
55 55 else:
56 56 return None
57 57
58 58
59 59 class TabWidget(AccordionWidget):
60 60 view_name = Unicode('TabView', sync=True)
General Comments 0
You need to be logged in to leave comments. Login now