##// END OF EJS Templates
Done with major changes,...
Jonathan Frederic -
Show More
@@ -1,196 +1,197
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "underscore",
6 6 "backbone",
7 7 ], function (_, Backbone) {
8 8
9 9 //--------------------------------------------------------------------
10 10 // WidgetManager class
11 11 //--------------------------------------------------------------------
12 var WidgetManager = function (comm_manager) {
12 var WidgetManager = function (comm_manager, keyboard_manager, notebook) {
13 13 // Public constructor
14 14 WidgetManager._managers.push(this);
15 15
16 16 // Attach a comm manager to the
17 this.keyboard_manager = keyboard_manager;
18 this.notebook = notebook;
17 19 this.comm_manager = comm_manager;
18 20 this._models = {}; /* Dictionary of model ids and model instances */
19 21
20 22 // Register already-registered widget model types with the comm manager.
21 23 var that = this;
22 24 _.each(WidgetManager._model_types, function(model_type, model_name) {
23 25 that.comm_manager.register_target(model_name, $.proxy(that._handle_comm_open, that));
24 26 });
25 27 };
26 28
27 29 //--------------------------------------------------------------------
28 30 // Class level
29 31 //--------------------------------------------------------------------
30 32 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
31 33 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
32 34 WidgetManager._managers = []; /* List of widget managers */
33 35
34 36 WidgetManager.register_widget_model = function (model_name, model_type) {
35 37 // Registers a widget model by name.
36 38 WidgetManager._model_types[model_name] = model_type;
37 39
38 40 // Register the widget with the comm manager. Make sure to pass this object's context
39 41 // in so `this` works in the call back.
40 42 _.each(WidgetManager._managers, function(instance, i) {
41 43 if (instance.comm_manager !== null) {
42 44 instance.comm_manager.register_target(model_name, $.proxy(instance._handle_comm_open, instance));
43 45 }
44 46 });
45 47 };
46 48
47 49 WidgetManager.register_widget_view = function (view_name, view_type) {
48 50 // Registers a widget view by name.
49 51 WidgetManager._view_types[view_name] = view_type;
50 52 };
51 53
52 54 //--------------------------------------------------------------------
53 55 // Instance level
54 56 //--------------------------------------------------------------------
55 57 WidgetManager.prototype.display_view = function(msg, model) {
56 58 // Displays a view for a particular model.
57 59 var cell = this.get_msg_cell(msg.parent_header.msg_id);
58 60 if (cell === null) {
59 61 console.log("Could not determine where the display" +
60 62 " message was from. Widget will not be displayed");
61 63 } else {
62 64 var view = this.create_view(model, {cell: cell});
63 65 if (view === null) {
64 66 console.error("View creation failed", model);
65 67 }
66 68 if (cell.widget_subarea) {
67 69 cell.widget_area.show();
68 70 this._handle_display_view(view);
69 71 cell.widget_subarea.append(view.$el);
70 72 view.trigger('displayed');
71 73 }
72 74 }
73 75 };
74 76
75 77 WidgetManager.prototype._handle_display_view = function (view) {
76 78 // Have the IPython keyboard manager disable its event
77 79 // handling so the widget can capture keyboard input.
78 80 // Note, this is only done on the outer most widgets.
79 IPython.keyboard_manager.register_events(view.$el);
81 if (this.keyboard_manager) {
82 this.keyboard_manager.register_events(view.$el);
80 83
81 84 if (view.additional_elements) {
82 85 for (var i = 0; i < view.additional_elements.length; i++) {
83 IPython.keyboard_manager.register_events(view.additional_elements[i]);
86 this.keyboard_manager.register_events(view.additional_elements[i]);
87 }
84 88 }
85 89 }
86 90 };
87 91
88 92 WidgetManager.prototype.create_view = function(model, options, view) {
89 93 // Creates a view for a particular model.
90 94 var view_name = model.get('_view_name');
91 95 var ViewType = WidgetManager._view_types[view_name];
92 96 if (ViewType) {
93 97
94 98 // If a view is passed into the method, use that view's cell as
95 99 // the cell for the view that is created.
96 100 options = options || {};
97 101 if (view !== undefined) {
98 102 options.cell = view.options.cell;
99 103 }
100 104
101 105 // Create and render the view...
102 106 var parameters = {model: model, options: options};
103 107 view = new ViewType(parameters);
104 108 view.render();
105 109 model.on('destroy', view.remove, view);
106 110 return view;
107 111 }
108 112 return null;
109 113 };
110 114
111 115 WidgetManager.prototype.get_msg_cell = function (msg_id) {
112 116 var cell = null;
113 117 // First, check to see if the msg was triggered by cell execution.
114 if (IPython.notebook) {
115 cell = IPython.notebook.get_msg_cell(msg_id);
118 if (this.notebook) {
119 cell = this.notebook.get_msg_cell(msg_id);
116 120 }
117 121 if (cell !== null) {
118 122 return cell;
119 123 }
120 124 // Second, check to see if a get_cell callback was defined
121 125 // for the message. get_cell callbacks are registered for
122 126 // widget messages, so this block is actually checking to see if the
123 127 // message was triggered by a widget.
124 128 var kernel = this.comm_manager.kernel;
125 129 if (kernel) {
126 130 var callbacks = kernel.get_callbacks_for_msg(msg_id);
127 131 if (callbacks && callbacks.iopub &&
128 132 callbacks.iopub.get_cell !== undefined) {
129 133 return callbacks.iopub.get_cell();
130 134 }
131 135 }
132 136
133 137 // Not triggered by a cell or widget (no get_cell callback
134 138 // exists).
135 139 return null;
136 140 };
137 141
138 142 WidgetManager.prototype.callbacks = function (view) {
139 143 // callback handlers specific a view
140 144 var callbacks = {};
141 145 if (view && view.options.cell) {
142 146
143 147 // Try to get output handlers
144 148 var cell = view.options.cell;
145 149 var handle_output = null;
146 150 var handle_clear_output = null;
147 151 if (cell.output_area) {
148 152 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
149 153 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
150 154 }
151 155
152 156 // Create callback dict using what is known
153 157 var that = this;
154 158 callbacks = {
155 159 iopub : {
156 160 output : handle_output,
157 161 clear_output : handle_clear_output,
158 162
159 163 // Special function only registered by widget messages.
160 164 // Allows us to get the cell for a message so we know
161 165 // where to add widgets if the code requires it.
162 166 get_cell : function () {
163 167 return cell;
164 168 },
165 169 },
166 170 };
167 171 }
168 172 return callbacks;
169 173 };
170 174
171 175 WidgetManager.prototype.get_model = function (model_id) {
172 176 // Look-up a model instance by its id.
173 177 var model = this._models[model_id];
174 178 if (model !== undefined && model.id == model_id) {
175 179 return model;
176 180 }
177 181 return null;
178 182 };
179 183
180 184 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
181 185 // Handle when a comm is opened.
182 186 var that = this;
183 187 var model_id = comm.comm_id;
184 188 var widget_type_name = msg.content.target_name;
185 189 var widget_model = new WidgetManager._model_types[widget_type_name](this, model_id, comm);
186 190 widget_model.on('comm:close', function () {
187 191 delete that._models[model_id];
188 192 });
189 193 this._models[model_id] = widget_model;
190 194 };
191 195
192 // For backwards compatability.
193 IPython.WidgetManager = WidgetManager;
194
195 196 return WidgetManager;
196 197 });
@@ -1,466 +1,466
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define(["widgets/js/manager",
5 5 "underscore",
6 6 "backbone"],
7 7 function(WidgetManager, _, Backbone){
8 8
9 9 var WidgetModel = Backbone.Model.extend({
10 10 constructor: function (widget_manager, model_id, comm) {
11 11 // Constructor
12 12 //
13 13 // Creates a WidgetModel instance.
14 14 //
15 15 // Parameters
16 16 // ----------
17 17 // widget_manager : WidgetManager instance
18 18 // model_id : string
19 19 // An ID unique to this model.
20 20 // comm : Comm instance (optional)
21 21 this.widget_manager = widget_manager;
22 22 this._buffered_state_diff = {};
23 23 this.pending_msgs = 0;
24 24 this.msg_buffer = null;
25 25 this.key_value_lock = null;
26 26 this.id = model_id;
27 27 this.views = [];
28 28
29 29 if (comm !== undefined) {
30 30 // Remember comm associated with the model.
31 31 this.comm = comm;
32 32 comm.model = this;
33 33
34 34 // Hook comm messages up to model.
35 35 comm.on_close($.proxy(this._handle_comm_closed, this));
36 36 comm.on_msg($.proxy(this._handle_comm_msg, this));
37 37 }
38 38 return Backbone.Model.apply(this);
39 39 },
40 40
41 41 send: function (content, callbacks) {
42 42 // Send a custom msg over the comm.
43 43 if (this.comm !== undefined) {
44 44 var data = {method: 'custom', content: content};
45 45 this.comm.send(data, callbacks);
46 46 this.pending_msgs++;
47 47 }
48 48 },
49 49
50 50 _handle_comm_closed: function (msg) {
51 51 // Handle when a widget is closed.
52 52 this.trigger('comm:close');
53 53 delete this.comm.model; // Delete ref so GC will collect widget model.
54 54 delete this.comm;
55 55 delete this.model_id; // Delete id from model so widget manager cleans up.
56 56 _.each(this.views, function(view, i) {
57 57 view.remove();
58 58 });
59 59 },
60 60
61 61 _handle_comm_msg: function (msg) {
62 62 // Handle incoming comm msg.
63 63 var method = msg.content.data.method;
64 64 switch (method) {
65 65 case 'update':
66 66 this.apply_update(msg.content.data.state);
67 67 break;
68 68 case 'custom':
69 69 this.trigger('msg:custom', msg.content.data.content);
70 70 break;
71 71 case 'display':
72 72 this.widget_manager.display_view(msg, this);
73 73 break;
74 74 }
75 75 },
76 76
77 77 apply_update: function (state) {
78 78 // Handle when a widget is updated via the python side.
79 79 var that = this;
80 80 _.each(state, function(value, key) {
81 81 that.key_value_lock = [key, value];
82 82 try {
83 83 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
84 84 } finally {
85 85 that.key_value_lock = null;
86 86 }
87 87 });
88 88 },
89 89
90 90 _handle_status: function (msg, callbacks) {
91 91 // Handle status msgs.
92 92
93 93 // execution_state : ('busy', 'idle', 'starting')
94 94 if (this.comm !== undefined) {
95 95 if (msg.content.execution_state ==='idle') {
96 96 // Send buffer if this message caused another message to be
97 97 // throttled.
98 98 if (this.msg_buffer !== null &&
99 99 (this.get('msg_throttle') || 3) === this.pending_msgs) {
100 100 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
101 101 this.comm.send(data, callbacks);
102 102 this.msg_buffer = null;
103 103 } else {
104 104 --this.pending_msgs;
105 105 }
106 106 }
107 107 }
108 108 },
109 109
110 110 callbacks: function(view) {
111 111 // Create msg callbacks for a comm msg.
112 112 var callbacks = this.widget_manager.callbacks(view);
113 113
114 114 if (callbacks.iopub === undefined) {
115 115 callbacks.iopub = {};
116 116 }
117 117
118 118 var that = this;
119 119 callbacks.iopub.status = function (msg) {
120 120 that._handle_status(msg, callbacks);
121 121 };
122 122 return callbacks;
123 123 },
124 124
125 125 set: function(key, val, options) {
126 126 // Set a value.
127 127 var return_value = WidgetModel.__super__.set.apply(this, arguments);
128 128
129 129 // Backbone only remembers the diff of the most recent set()
130 130 // operation. Calling set multiple times in a row results in a
131 131 // loss of diff information. Here we keep our own running diff.
132 132 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
133 133 return return_value;
134 134 },
135 135
136 136 sync: function (method, model, options) {
137 137 // Handle sync to the back-end. Called when a model.save() is called.
138 138
139 139 // Make sure a comm exists.
140 140 var error = options.error || function() {
141 141 console.error('Backbone sync error:', arguments);
142 142 };
143 143 if (this.comm === undefined) {
144 144 error();
145 145 return false;
146 146 }
147 147
148 148 // Delete any key value pairs that the back-end already knows about.
149 149 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
150 150 if (this.key_value_lock !== null) {
151 151 var key = this.key_value_lock[0];
152 152 var value = this.key_value_lock[1];
153 153 if (attrs[key] === value) {
154 154 delete attrs[key];
155 155 }
156 156 }
157 157
158 158 // Only sync if there are attributes to send to the back-end.
159 159 attrs = this._pack_models(attrs);
160 160 if (_.size(attrs) > 0) {
161 161
162 162 // If this message was sent via backbone itself, it will not
163 163 // have any callbacks. It's important that we create callbacks
164 164 // so we can listen for status messages, etc...
165 165 var callbacks = options.callbacks || this.callbacks();
166 166
167 167 // Check throttle.
168 168 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
169 169 // The throttle has been exceeded, buffer the current msg so
170 170 // it can be sent once the kernel has finished processing
171 171 // some of the existing messages.
172 172
173 173 // Combine updates if it is a 'patch' sync, otherwise replace updates
174 174 switch (method) {
175 175 case 'patch':
176 176 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
177 177 break;
178 178 case 'update':
179 179 case 'create':
180 180 this.msg_buffer = attrs;
181 181 break;
182 182 default:
183 183 error();
184 184 return false;
185 185 }
186 186 this.msg_buffer_callbacks = callbacks;
187 187
188 188 } else {
189 189 // We haven't exceeded the throttle, send the message like
190 190 // normal.
191 191 var data = {method: 'backbone', sync_data: attrs};
192 192 this.comm.send(data, callbacks);
193 193 this.pending_msgs++;
194 194 }
195 195 }
196 196 // Since the comm is a one-way communication, assume the message
197 197 // arrived. Don't call success since we don't have a model back from the server
198 198 // this means we miss out on the 'sync' event.
199 199 this._buffered_state_diff = {};
200 200 },
201 201
202 202 save_changes: function(callbacks) {
203 203 // Push this model's state to the back-end
204 204 //
205 205 // This invokes a Backbone.Sync.
206 206 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
207 207 },
208 208
209 209 _pack_models: function(value) {
210 210 // Replace models with model ids recursively.
211 var that = this;
212 var packed;
211 213 if (value instanceof Backbone.Model) {
212 214 return value.id;
213 215
214 216 } else if ($.isArray(value)) {
215 var packed = [];
216 var that = this;
217 packed = [];
217 218 _.each(value, function(sub_value, key) {
218 219 packed.push(that._pack_models(sub_value));
219 220 });
220 221 return packed;
221 222
222 223 } else if (value instanceof Object) {
223 var packed = {};
224 var that = this;
224 packed = {};
225 225 _.each(value, function(sub_value, key) {
226 226 packed[key] = that._pack_models(sub_value);
227 227 });
228 228 return packed;
229 229
230 230 } else {
231 231 return value;
232 232 }
233 233 },
234 234
235 235 _unpack_models: function(value) {
236 236 // Replace model ids with models recursively.
237 237 var that = this;
238 238 var unpacked;
239 239 if ($.isArray(value)) {
240 240 unpacked = [];
241 241 _.each(value, function(sub_value, key) {
242 242 unpacked.push(that._unpack_models(sub_value));
243 243 });
244 244 return unpacked;
245 245
246 246 } else if (value instanceof Object) {
247 247 unpacked = {};
248 248 _.each(value, function(sub_value, key) {
249 249 unpacked[key] = that._unpack_models(sub_value);
250 250 });
251 251 return unpacked;
252 252
253 253 } else {
254 254 var model = this.widget_manager.get_model(value);
255 255 if (model) {
256 256 return model;
257 257 } else {
258 258 return value;
259 259 }
260 260 }
261 261 },
262 262
263 263 });
264 264 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
265 265
266 266
267 267 var WidgetView = Backbone.View.extend({
268 268 initialize: function(parameters) {
269 269 // Public constructor.
270 270 this.model.on('change',this.update,this);
271 271 this.options = parameters.options;
272 272 this.child_model_views = {};
273 273 this.child_views = {};
274 274 this.model.views.push(this);
275 275 this.id = this.id || IPython.utils.uuid();
276 276 },
277 277
278 278 update: function(){
279 279 // Triggered on model change.
280 280 //
281 281 // Update view to be consistent with this.model
282 282 },
283 283
284 284 create_child_view: function(child_model, options) {
285 285 // Create and return a child view.
286 286 //
287 287 // -given a model and (optionally) a view name if the view name is
288 288 // not given, it defaults to the model's default view attribute.
289 289
290 290 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
291 291 // it would be great to have the widget manager add the cell metadata
292 292 // to the subview without having to add it here.
293 293 options = $.extend({ parent: this }, options || {});
294 294 var child_view = this.model.widget_manager.create_view(child_model, options, this);
295 295
296 296 // Associate the view id with the model id.
297 297 if (this.child_model_views[child_model.id] === undefined) {
298 298 this.child_model_views[child_model.id] = [];
299 299 }
300 300 this.child_model_views[child_model.id].push(child_view.id);
301 301
302 302 // Remember the view by id.
303 303 this.child_views[child_view.id] = child_view;
304 304 return child_view;
305 305 },
306 306
307 307 pop_child_view: function(child_model) {
308 308 // Delete a child view that was previously created using create_child_view.
309 309 var view_ids = this.child_model_views[child_model.id];
310 310 if (view_ids !== undefined) {
311 311
312 312 // Only delete the first view in the list.
313 313 var view_id = view_ids[0];
314 314 var view = this.child_views[view_id];
315 315 delete this.child_views[view_id];
316 316 view_ids.splice(0,1);
317 317 child_model.views.pop(view);
318 318
319 319 // Remove the view list specific to this model if it is empty.
320 320 if (view_ids.length === 0) {
321 321 delete this.child_model_views[child_model.id];
322 322 }
323 323 return view;
324 324 }
325 325 return null;
326 326 },
327 327
328 328 do_diff: function(old_list, new_list, removed_callback, added_callback) {
329 329 // Difference a changed list and call remove and add callbacks for
330 330 // each removed and added item in the new list.
331 331 //
332 332 // Parameters
333 333 // ----------
334 334 // old_list : array
335 335 // new_list : array
336 336 // removed_callback : Callback(item)
337 337 // Callback that is called for each item removed.
338 338 // added_callback : Callback(item)
339 339 // Callback that is called for each item added.
340 340
341 341 // Walk the lists until an unequal entry is found.
342 342 var i;
343 343 for (i = 0; i < new_list.length; i++) {
344 344 if (i < old_list.length || new_list[i] !== old_list[i]) {
345 345 break;
346 346 }
347 347 }
348 348
349 349 // Remove the non-matching items from the old list.
350 350 for (var j = i; j < old_list.length; j++) {
351 351 removed_callback(old_list[j]);
352 352 }
353 353
354 354 // Add the rest of the new list items.
355 355 for (i; i < new_list.length; i++) {
356 356 added_callback(new_list[i]);
357 357 }
358 358 },
359 359
360 360 callbacks: function(){
361 361 // Create msg callbacks for a comm msg.
362 362 return this.model.callbacks(this);
363 363 },
364 364
365 365 render: function(){
366 366 // Render the view.
367 367 //
368 368 // By default, this is only called the first time the view is created
369 369 },
370 370
371 371 send: function (content) {
372 372 // Send a custom msg associated with this view.
373 373 this.model.send(content, this.callbacks());
374 374 },
375 375
376 376 touch: function () {
377 377 this.model.save_changes(this.callbacks());
378 378 },
379 379 });
380 380
381 381
382 382 var DOMWidgetView = WidgetView.extend({
383 383 initialize: function (options) {
384 384 // Public constructor
385 385
386 386 // In the future we may want to make changes more granular
387 387 // (e.g., trigger on visible:change).
388 388 this.model.on('change', this.update, this);
389 389 this.model.on('msg:custom', this.on_msg, this);
390 390 DOMWidgetView.__super__.initialize.apply(this, arguments);
391 391 },
392 392
393 393 on_msg: function(msg) {
394 394 // Handle DOM specific msgs.
395 395 switch(msg.msg_type) {
396 396 case 'add_class':
397 397 this.add_class(msg.selector, msg.class_list);
398 398 break;
399 399 case 'remove_class':
400 400 this.remove_class(msg.selector, msg.class_list);
401 401 break;
402 402 }
403 403 },
404 404
405 405 add_class: function (selector, class_list) {
406 406 // Add a DOM class to an element.
407 407 this._get_selector_element(selector).addClass(class_list);
408 408 },
409 409
410 410 remove_class: function (selector, class_list) {
411 411 // Remove a DOM class from an element.
412 412 this._get_selector_element(selector).removeClass(class_list);
413 413 },
414 414
415 415 update: function () {
416 416 // Update the contents of this view
417 417 //
418 418 // Called when the model is changed. The model may have been
419 419 // changed by another view or by a state update from the back-end.
420 420 // The very first update seems to happen before the element is
421 421 // finished rendering so we use setTimeout to give the element time
422 422 // to render
423 423 var e = this.$el;
424 424 var visible = this.model.get('visible');
425 425 setTimeout(function() {e.toggle(visible);},0);
426 426
427 427 var css = this.model.get('_css');
428 428 if (css === undefined) {return;}
429 429 for (var i = 0; i < css.length; i++) {
430 430 // Apply the css traits to all elements that match the selector.
431 431 var selector = css[i][0];
432 432 var elements = this._get_selector_element(selector);
433 433 if (elements.length > 0) {
434 434 var trait_key = css[i][1];
435 435 var trait_value = css[i][2];
436 436 elements.css(trait_key ,trait_value);
437 437 }
438 438 }
439 439 },
440 440
441 441 _get_selector_element: function (selector) {
442 442 // Get the elements via the css selector.
443 443
444 444 // If the selector is blank, apply the style to the $el_to_style
445 445 // element. If the $el_to_style element is not defined, use apply
446 446 // the style to the view's element.
447 447 var elements;
448 448 if (!selector) {
449 449 if (this.$el_to_style === undefined) {
450 450 elements = this.$el;
451 451 } else {
452 452 elements = this.$el_to_style;
453 453 }
454 454 } else {
455 455 elements = this.$el.find(selector);
456 456 }
457 457 return elements;
458 458 },
459 459 });
460 460
461 461 return {
462 462 'WidgetModel': WidgetModel,
463 463 'WidgetView': WidgetView,
464 464 'DOMWidgetView': DOMWidgetView,
465 465 };
466 466 });
@@ -1,377 +1,378
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "widgets/js/widget",
6 ], function(widget){
6 "base/js/utils",
7 ], function(widget, utils){
7 8
8 9 var DropdownView = widget.DOMWidgetView.extend({
9 10 render : function(){
10 11 // Called when view is rendered.
11 12 this.$el
12 13 .addClass('widget-hbox-single');
13 14 this.$label = $('<div />')
14 15 .appendTo(this.$el)
15 16 .addClass('widget-hlabel')
16 17 .hide();
17 18 this.$buttongroup = $('<div />')
18 19 .addClass('widget_item')
19 20 .addClass('btn-group')
20 21 .appendTo(this.$el);
21 22 this.$el_to_style = this.$buttongroup; // Set default element to style
22 23 this.$droplabel = $('<button />')
23 24 .addClass('btn btn-default')
24 25 .addClass('widget-combo-btn')
25 26 .html("&nbsp;")
26 27 .appendTo(this.$buttongroup);
27 28 this.$dropbutton = $('<button />')
28 29 .addClass('btn btn-default')
29 30 .addClass('dropdown-toggle')
30 31 .addClass('widget-combo-carrot-btn')
31 32 .attr('data-toggle', 'dropdown')
32 33 .append($('<span />').addClass("caret"))
33 34 .appendTo(this.$buttongroup);
34 35 this.$droplist = $('<ul />')
35 36 .addClass('dropdown-menu')
36 37 .appendTo(this.$buttongroup);
37 38
38 39 // Set defaults.
39 40 this.update();
40 41 },
41 42
42 43 update : function(options){
43 44 // Update the contents of this view
44 45 //
45 46 // Called when the model is changed. The model may have been
46 47 // changed by another view or by a state update from the back-end.
47 48
48 49 if (options === undefined || options.updated_view != this) {
49 50 var selected_item_text = this.model.get('value_name');
50 51 if (selected_item_text.trim().length === 0) {
51 52 this.$droplabel.html("&nbsp;");
52 53 } else {
53 54 this.$droplabel.text(selected_item_text);
54 55 }
55 56
56 57 var items = this.model.get('value_names');
57 58 var $replace_droplist = $('<ul />')
58 59 .addClass('dropdown-menu');
59 60 var that = this;
60 61 _.each(items, function(item, i) {
61 62 var item_button = $('<a href="#"/>')
62 63 .text(item)
63 64 .on('click', $.proxy(that.handle_click, that));
64 65 $replace_droplist.append($('<li />').append(item_button));
65 66 });
66 67
67 68 this.$droplist.replaceWith($replace_droplist);
68 69 this.$droplist.remove();
69 70 this.$droplist = $replace_droplist;
70 71
71 72 if (this.model.get('disabled')) {
72 73 this.$buttongroup.attr('disabled','disabled');
73 74 this.$droplabel.attr('disabled','disabled');
74 75 this.$dropbutton.attr('disabled','disabled');
75 76 this.$droplist.attr('disabled','disabled');
76 77 } else {
77 78 this.$buttongroup.removeAttr('disabled');
78 79 this.$droplabel.removeAttr('disabled');
79 80 this.$dropbutton.removeAttr('disabled');
80 81 this.$droplist.removeAttr('disabled');
81 82 }
82 83
83 84 var description = this.model.get('description');
84 85 if (description.length === 0) {
85 86 this.$label.hide();
86 87 } else {
87 88 this.$label.text(description);
88 89 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
89 90 this.$label.show();
90 91 }
91 92 }
92 93 return DropdownView.__super__.update.apply(this);
93 94 },
94 95
95 96 handle_click: function (e) {
96 97 // Handle when a value is clicked.
97 98
98 99 // Calling model.set will trigger all of the other views of the
99 100 // model to update.
100 101 this.model.set('value_name', $(e.target).text(), {updated_view: this});
101 102 this.touch();
102 103 },
103 104
104 105 });
105 106
106 107
107 108 var RadioButtonsView = widget.DOMWidgetView.extend({
108 109 render : function(){
109 110 // Called when view is rendered.
110 111 this.$el
111 112 .addClass('widget-hbox');
112 113 this.$label = $('<div />')
113 114 .appendTo(this.$el)
114 115 .addClass('widget-hlabel')
115 116 .hide();
116 117 this.$container = $('<div />')
117 118 .appendTo(this.$el)
118 119 .addClass('widget-radio-box');
119 120 this.$el_to_style = this.$container; // Set default element to style
120 121 this.update();
121 122 },
122 123
123 124 update : function(options){
124 125 // Update the contents of this view
125 126 //
126 127 // Called when the model is changed. The model may have been
127 128 // changed by another view or by a state update from the back-end.
128 129 if (options === undefined || options.updated_view != this) {
129 130 // Add missing items to the DOM.
130 131 var items = this.model.get('value_names');
131 132 var disabled = this.model.get('disabled');
132 133 var that = this;
133 134 _.each(items, function(item, index) {
134 135 var item_query = ' :input[value="' + item + '"]';
135 136 if (that.$el.find(item_query).length === 0) {
136 137 var $label = $('<label />')
137 138 .addClass('radio')
138 139 .text(item)
139 140 .appendTo(that.$container);
140 141
141 142 $('<input />')
142 143 .attr('type', 'radio')
143 144 .addClass(that.model)
144 145 .val(item)
145 146 .prependTo($label)
146 147 .on('click', $.proxy(that.handle_click, that));
147 148 }
148 149
149 150 var $item_element = that.$container.find(item_query);
150 151 if (that.model.get('value_name') == item) {
151 152 $item_element.prop('checked', true);
152 153 } else {
153 154 $item_element.prop('checked', false);
154 155 }
155 156 $item_element.prop('disabled', disabled);
156 157 });
157 158
158 159 // Remove items that no longer exist.
159 160 this.$container.find('input').each(function(i, obj) {
160 161 var value = $(obj).val();
161 162 var found = false;
162 163 _.each(items, function(item, index) {
163 164 if (item == value) {
164 165 found = true;
165 166 return false;
166 167 }
167 168 });
168 169
169 170 if (!found) {
170 171 $(obj).parent().remove();
171 172 }
172 173 });
173 174
174 175 var description = this.model.get('description');
175 176 if (description.length === 0) {
176 177 this.$label.hide();
177 178 } else {
178 179 this.$label.text(description);
179 180 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
180 181 this.$label.show();
181 182 }
182 183 }
183 184 return RadioButtonsView.__super__.update.apply(this);
184 185 },
185 186
186 187 handle_click: function (e) {
187 188 // Handle when a value is clicked.
188 189
189 190 // Calling model.set will trigger all of the other views of the
190 191 // model to update.
191 192 this.model.set('value_name', $(e.target).val(), {updated_view: this});
192 193 this.touch();
193 194 },
194 195 });
195 196
196 197
197 198 var ToggleButtonsView = widget.DOMWidgetView.extend({
198 199 render : function(){
199 200 // Called when view is rendered.
200 201 this.$el
201 202 .addClass('widget-hbox-single');
202 203 this.$label = $('<div />')
203 204 .appendTo(this.$el)
204 205 .addClass('widget-hlabel')
205 206 .hide();
206 207 this.$buttongroup = $('<div />')
207 208 .addClass('btn-group')
208 209 .attr('data-toggle', 'buttons-radio')
209 210 .appendTo(this.$el);
210 211 this.$el_to_style = this.$buttongroup; // Set default element to style
211 212 this.update();
212 213 },
213 214
214 215 update : function(options){
215 216 // Update the contents of this view
216 217 //
217 218 // Called when the model is changed. The model may have been
218 219 // changed by another view or by a state update from the back-end.
219 220 if (options === undefined || options.updated_view != this) {
220 221 // Add missing items to the DOM.
221 222 var items = this.model.get('value_names');
222 223 var disabled = this.model.get('disabled');
223 224 var that = this;
224 225 var item_html;
225 226 _.each(items, function(item, index) {
226 227 if (item.trim().length == 0) {
227 228 item_html = "&nbsp;";
228 229 } else {
229 item_html = IPython.utils.escape_html(item);
230 item_html = utils.escape_html(item);
230 231 }
231 232 var item_query = '[data-value="' + item + '"]';
232 233 var $item_element = that.$buttongroup.find(item_query);
233 234 if (!$item_element.length) {
234 235 $item_element = $('<button/>')
235 236 .attr('type', 'button')
236 237 .addClass('btn btn-default')
237 238 .html(item_html)
238 239 .appendTo(that.$buttongroup)
239 240 .attr('data-value', item)
240 241 .on('click', $.proxy(that.handle_click, that));
241 242 }
242 243 if (that.model.get('value_name') == item) {
243 244 $item_element.addClass('active');
244 245 } else {
245 246 $item_element.removeClass('active');
246 247 }
247 248 $item_element.prop('disabled', disabled);
248 249 });
249 250
250 251 // Remove items that no longer exist.
251 252 this.$buttongroup.find('button').each(function(i, obj) {
252 253 var value = $(obj).data('value');
253 254 var found = false;
254 255 _.each(items, function(item, index) {
255 256 if (item == value) {
256 257 found = true;
257 258 return false;
258 259 }
259 260 });
260 261
261 262 if (!found) {
262 263 $(obj).remove();
263 264 }
264 265 });
265 266
266 267 var description = this.model.get('description');
267 268 if (description.length === 0) {
268 269 this.$label.hide();
269 270 } else {
270 271 this.$label.text(description);
271 272 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
272 273 this.$label.show();
273 274 }
274 275 }
275 276 return ToggleButtonsView.__super__.update.apply(this);
276 277 },
277 278
278 279 handle_click: function (e) {
279 280 // Handle when a value is clicked.
280 281
281 282 // Calling model.set will trigger all of the other views of the
282 283 // model to update.
283 284 this.model.set('value_name', $(e.target).data('value'), {updated_view: this});
284 285 this.touch();
285 286 },
286 287 });
287 288
288 289
289 290 var SelectView = widget.DOMWidgetView.extend({
290 291 render : function(){
291 292 // Called when view is rendered.
292 293 this.$el
293 294 .addClass('widget-hbox');
294 295 this.$label = $('<div />')
295 296 .appendTo(this.$el)
296 297 .addClass('widget-hlabel')
297 298 .hide();
298 299 this.$listbox = $('<select />')
299 300 .addClass('widget-listbox form-control')
300 301 .attr('size', 6)
301 302 .appendTo(this.$el);
302 303 this.$el_to_style = this.$listbox; // Set default element to style
303 304 this.update();
304 305 },
305 306
306 307 update : function(options){
307 308 // Update the contents of this view
308 309 //
309 310 // Called when the model is changed. The model may have been
310 311 // changed by another view or by a state update from the back-end.
311 312 if (options === undefined || options.updated_view != this) {
312 313 // Add missing items to the DOM.
313 314 var items = this.model.get('value_names');
314 315 var that = this;
315 316 _.each(items, function(item, index) {
316 317 var item_query = ' :contains("' + item + '")';
317 318 if (that.$listbox.find(item_query).length === 0) {
318 319 $('<option />')
319 320 .text(item)
320 321 .attr('value_name', item)
321 322 .appendTo(that.$listbox)
322 323 .on('click', $.proxy(that.handle_click, that));
323 324 }
324 325 });
325 326
326 327 // Select the correct element
327 328 this.$listbox.val(this.model.get('value_name'));
328 329
329 330 // Disable listbox if needed
330 331 var disabled = this.model.get('disabled');
331 332 this.$listbox.prop('disabled', disabled);
332 333
333 334 // Remove items that no longer exist.
334 335 this.$listbox.find('option').each(function(i, obj) {
335 336 var value = $(obj).text();
336 337 var found = false;
337 338 _.each(items, function(item, index) {
338 339 if (item == value) {
339 340 found = true;
340 341 return false;
341 342 }
342 343 });
343 344
344 345 if (!found) {
345 346 $(obj).remove();
346 347 }
347 348 });
348 349
349 350 var description = this.model.get('description');
350 351 if (description.length === 0) {
351 352 this.$label.hide();
352 353 } else {
353 354 this.$label.text(description);
354 355 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
355 356 this.$label.show();
356 357 }
357 358 }
358 359 return SelectView.__super__.update.apply(this);
359 360 },
360 361
361 362 handle_click: function (e) {
362 363 // Handle when a value is clicked.
363 364
364 365 // Calling model.set will trigger all of the other views of the
365 366 // model to update.
366 367 this.model.set('value_name', $(e.target).text(), {updated_view: this});
367 368 this.touch();
368 369 },
369 370 });
370 371
371 372 return {
372 373 'DropdownView': DropdownView,
373 374 'RadioButtonsView': RadioButtonsView,
374 375 'ToggleButtonsView': ToggleButtonsView,
375 376 'SelectView': SelectView,
376 377 };
377 378 });
@@ -1,264 +1,265
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "widgets/js/widget",
6 ], function(widget){
6 "base/js/utils",
7 ], function(widget, utils){
7 8
8 9 var AccordionView = widget.DOMWidgetView.extend({
9 10 render: function(){
10 11 // Called when view is rendered.
11 var guid = 'panel-group' + IPython.utils.uuid();
12 var guid = 'panel-group' + utils.uuid();
12 13 this.$el
13 14 .attr('id', guid)
14 15 .addClass('panel-group');
15 16 this.containers = [];
16 17 this.model_containers = {};
17 18 this.update_children([], this.model.get('children'));
18 19 this.model.on('change:children', function(model, value, options) {
19 20 this.update_children(model.previous('children'), value);
20 21 }, this);
21 22 this.model.on('change:selected_index', function(model, value, options) {
22 23 this.update_selected_index(model.previous('selected_index'), value, options);
23 24 }, this);
24 25 this.model.on('change:_titles', function(model, value, options) {
25 26 this.update_titles(value);
26 27 }, this);
27 28 var that = this;
28 29 this.on('displayed', function() {
29 30 this.update_titles();
30 31 // Trigger model displayed events for any models that are child to
31 32 // this model when this model is displayed.
32 33 that.is_displayed = true;
33 34 for (var property in that.child_views) {
34 35 if (that.child_views.hasOwnProperty(property)) {
35 36 that.child_views[property].trigger('displayed');
36 37 }
37 38 }
38 39 }, this);
39 40 },
40 41
41 42 update_titles: function(titles) {
42 43 // Set tab titles
43 44 if (!titles) {
44 45 titles = this.model.get('_titles');
45 46 }
46 47
47 48 var that = this;
48 49 _.each(titles, function(title, page_index) {
49 50 var accordian = that.containers[page_index];
50 51 if (accordian !== undefined) {
51 52 accordian
52 53 .find('.panel-heading')
53 54 .find('.accordion-toggle')
54 55 .text(title);
55 56 }
56 57 });
57 58 },
58 59
59 60 update_selected_index: function(old_index, new_index, options) {
60 61 // Only update the selection if the selection wasn't triggered
61 62 // by the front-end. It must be triggered by the back-end.
62 63 if (options === undefined || options.updated_view != this) {
63 64 this.containers[old_index].find('.panel-collapse').collapse('hide');
64 65 if (0 <= new_index && new_index < this.containers.length) {
65 66 this.containers[new_index].find('.panel-collapse').collapse('show');
66 67 }
67 68 }
68 69 },
69 70
70 71 update_children: function(old_list, new_list) {
71 72 // Called when the children list is modified.
72 73 this.do_diff(old_list,
73 74 new_list,
74 75 $.proxy(this.remove_child_model, this),
75 76 $.proxy(this.add_child_model, this));
76 77 },
77 78
78 79 remove_child_model: function(model) {
79 80 // Called when a child is removed from children list.
80 81 var accordion_group = this.model_containers[model.id];
81 82 this.containers.splice(accordion_group.container_index, 1);
82 83 delete this.model_containers[model.id];
83 84 accordion_group.remove();
84 85 this.pop_child_view(model);
85 86 },
86 87
87 88 add_child_model: function(model) {
88 89 // Called when a child is added to children list.
89 90 var view = this.create_child_view(model);
90 91 var index = this.containers.length;
91 var uuid = IPython.utils.uuid();
92 var uuid = utils.uuid();
92 93 var accordion_group = $('<div />')
93 94 .addClass('panel panel-default')
94 95 .appendTo(this.$el);
95 96 var accordion_heading = $('<div />')
96 97 .addClass('panel-heading')
97 98 .appendTo(accordion_group);
98 99 var that = this;
99 100 var accordion_toggle = $('<a />')
100 101 .addClass('accordion-toggle')
101 102 .attr('data-toggle', 'collapse')
102 103 .attr('data-parent', '#' + this.$el.attr('id'))
103 104 .attr('href', '#' + uuid)
104 105 .click(function(evt){
105 106
106 107 // Calling model.set will trigger all of the other views of the
107 108 // model to update.
108 109 that.model.set("selected_index", index, {updated_view: that});
109 110 that.touch();
110 111 })
111 112 .text('Page ' + index)
112 113 .appendTo(accordion_heading);
113 114 var accordion_body = $('<div />', {id: uuid})
114 115 .addClass('panel-collapse collapse')
115 116 .appendTo(accordion_group);
116 117 var accordion_inner = $('<div />')
117 118 .addClass('panel-body')
118 119 .appendTo(accordion_body);
119 120 var container_index = this.containers.push(accordion_group) - 1;
120 121 accordion_group.container_index = container_index;
121 122 this.model_containers[model.id] = accordion_group;
122 123 accordion_inner.append(view.$el);
123 124
124 125 this.update();
125 126 this.update_titles();
126 127
127 128 // Trigger the displayed event if this model is displayed.
128 129 if (this.is_displayed) {
129 130 view.trigger('displayed');
130 131 }
131 132 },
132 133 });
133 134
134 135
135 136 var TabView = widget.DOMWidgetView.extend({
136 137 initialize: function() {
137 138 // Public constructor.
138 139 this.containers = [];
139 140 TabView.__super__.initialize.apply(this, arguments);
140 141 },
141 142
142 143 render: function(){
143 144 // Called when view is rendered.
144 var uuid = 'tabs'+IPython.utils.uuid();
145 var uuid = 'tabs'+utils.uuid();
145 146 var that = this;
146 147 this.$tabs = $('<div />', {id: uuid})
147 148 .addClass('nav')
148 149 .addClass('nav-tabs')
149 150 .appendTo(this.$el);
150 151 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
151 152 .addClass('tab-content')
152 153 .appendTo(this.$el);
153 154 this.containers = [];
154 155 this.update_children([], this.model.get('children'));
155 156 this.model.on('change:children', function(model, value, options) {
156 157 this.update_children(model.previous('children'), value);
157 158 }, this);
158 159
159 160 // Trigger model displayed events for any models that are child to
160 161 // this model when this model is displayed.
161 162 this.on('displayed', function(){
162 163 that.is_displayed = true;
163 164 for (var property in that.child_views) {
164 165 if (that.child_views.hasOwnProperty(property)) {
165 166 that.child_views[property].trigger('displayed');
166 167 }
167 168 }
168 169 });
169 170 },
170 171
171 172 update_children: function(old_list, new_list) {
172 173 // Called when the children list is modified.
173 174 this.do_diff(old_list,
174 175 new_list,
175 176 $.proxy(this.remove_child_model, this),
176 177 $.proxy(this.add_child_model, this));
177 178 },
178 179
179 180 remove_child_model: function(model) {
180 181 // Called when a child is removed from children list.
181 182 var view = this.pop_child_view(model);
182 183 this.containers.splice(view.parent_tab.tab_text_index, 1);
183 184 view.parent_tab.remove();
184 185 view.parent_container.remove();
185 186 view.remove();
186 187 },
187 188
188 189 add_child_model: function(model) {
189 190 // Called when a child is added to children list.
190 191 var view = this.create_child_view(model);
191 192 var index = this.containers.length;
192 var uuid = IPython.utils.uuid();
193 var uuid = utils.uuid();
193 194
194 195 var that = this;
195 196 var tab = $('<li />')
196 197 .css('list-style-type', 'none')
197 198 .appendTo(this.$tabs);
198 199 view.parent_tab = tab;
199 200
200 201 var tab_text = $('<a />')
201 202 .attr('href', '#' + uuid)
202 203 .attr('data-toggle', 'tab')
203 204 .text('Page ' + index)
204 205 .appendTo(tab)
205 206 .click(function (e) {
206 207
207 208 // Calling model.set will trigger all of the other views of the
208 209 // model to update.
209 210 that.model.set("selected_index", index, {updated_view: this});
210 211 that.touch();
211 212 that.select_page(index);
212 213 });
213 214 tab.tab_text_index = this.containers.push(tab_text) - 1;
214 215
215 216 var contents_div = $('<div />', {id: uuid})
216 217 .addClass('tab-pane')
217 218 .addClass('fade')
218 219 .append(view.$el)
219 220 .appendTo(this.$tab_contents);
220 221 view.parent_container = contents_div;
221 222
222 223 // Trigger the displayed event if this model is displayed.
223 224 if (this.is_displayed) {
224 225 view.trigger('displayed');
225 226 }
226 227 },
227 228
228 229 update: function(options) {
229 230 // Update the contents of this view
230 231 //
231 232 // Called when the model is changed. The model may have been
232 233 // changed by another view or by a state update from the back-end.
233 234 if (options === undefined || options.updated_view != this) {
234 235 // Set tab titles
235 236 var titles = this.model.get('_titles');
236 237 var that = this;
237 238 _.each(titles, function(title, page_index) {
238 239 var tab_text = that.containers[page_index];
239 240 if (tab_text !== undefined) {
240 241 tab_text.text(title);
241 242 }
242 243 });
243 244
244 245 var selected_index = this.model.get('selected_index');
245 246 if (0 <= selected_index && selected_index < this.containers.length) {
246 247 this.select_page(selected_index);
247 248 }
248 249 }
249 250 return TabView.__super__.update.apply(this);
250 251 },
251 252
252 253 select_page: function(index) {
253 254 // Select a page.
254 255 this.$tabs.find('li')
255 256 .removeClass('active');
256 257 this.containers[index].tab('show');
257 258 },
258 259 });
259 260
260 261 return {
261 262 'AccordionView': AccordionView,
262 263 'TabView': TabView,
263 264 };
264 265 });
General Comments 0
You need to be logged in to leave comments. Login now