##// END OF EJS Templates
Merge pull request #6548 from SylvainCorlay/widget_views_by_id...
Jonathan Frederic -
r18036:e9b674db merge
parent child Browse files
Show More
@@ -1,607 +1,609 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define(["widgets/js/manager",
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/namespace",
9 9 ], function(widgetmanager, _, Backbone, $, IPython){
10 10
11 11 var WidgetModel = Backbone.Model.extend({
12 12 constructor: function (widget_manager, model_id, comm) {
13 13 // Constructor
14 14 //
15 15 // Creates a WidgetModel instance.
16 16 //
17 17 // Parameters
18 18 // ----------
19 19 // widget_manager : WidgetManager instance
20 20 // model_id : string
21 21 // An ID unique to this model.
22 22 // comm : Comm instance (optional)
23 23 this.widget_manager = widget_manager;
24 24 this._buffered_state_diff = {};
25 25 this.pending_msgs = 0;
26 26 this.msg_buffer = null;
27 27 this.state_lock = null;
28 28 this.id = model_id;
29 this.views = [];
29 this.views = {};
30 30
31 31 if (comm !== undefined) {
32 32 // Remember comm associated with the model.
33 33 this.comm = comm;
34 34 comm.model = this;
35 35
36 36 // Hook comm messages up to model.
37 37 comm.on_close($.proxy(this._handle_comm_closed, this));
38 38 comm.on_msg($.proxy(this._handle_comm_msg, this));
39 39 }
40 40 return Backbone.Model.apply(this);
41 41 },
42 42
43 43 send: function (content, callbacks) {
44 44 // Send a custom msg over the comm.
45 45 if (this.comm !== undefined) {
46 46 var data = {method: 'custom', content: content};
47 47 this.comm.send(data, callbacks);
48 48 this.pending_msgs++;
49 49 }
50 50 },
51 51
52 52 _handle_comm_closed: function (msg) {
53 53 // Handle when a widget is closed.
54 54 this.trigger('comm:close');
55 55 this.stopListening();
56 56 this.trigger('destroy', this);
57 57 delete this.comm.model; // Delete ref so GC will collect widget model.
58 58 delete this.comm;
59 59 delete this.model_id; // Delete id from model so widget manager cleans up.
60 _.each(this.views, function(view, i) {
61 view.remove();
62 });
60 for (var id in this.views) {
61 if (this.views.hasOwnProperty(id)) {
62 this.views[id].remove();
63 }
64 }
63 65 },
64 66
65 67 _handle_comm_msg: function (msg) {
66 68 // Handle incoming comm msg.
67 69 var method = msg.content.data.method;
68 70 switch (method) {
69 71 case 'update':
70 72 this.apply_update(msg.content.data.state);
71 73 break;
72 74 case 'custom':
73 75 this.trigger('msg:custom', msg.content.data.content);
74 76 break;
75 77 case 'display':
76 78 this.widget_manager.display_view(msg, this);
77 79 break;
78 80 }
79 81 },
80 82
81 83 apply_update: function (state) {
82 84 // Handle when a widget is updated via the python side.
83 85 this.state_lock = state;
84 86 try {
85 87 var that = this;
86 88 WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
87 89 obj[key] = that._unpack_models(state[key]);
88 90 return obj;
89 91 }, {})]);
90 92 } finally {
91 93 this.state_lock = null;
92 94 }
93 95 },
94 96
95 97 _handle_status: function (msg, callbacks) {
96 98 // Handle status msgs.
97 99
98 100 // execution_state : ('busy', 'idle', 'starting')
99 101 if (this.comm !== undefined) {
100 102 if (msg.content.execution_state ==='idle') {
101 103 // Send buffer if this message caused another message to be
102 104 // throttled.
103 105 if (this.msg_buffer !== null &&
104 106 (this.get('msg_throttle') || 3) === this.pending_msgs) {
105 107 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
106 108 this.comm.send(data, callbacks);
107 109 this.msg_buffer = null;
108 110 } else {
109 111 --this.pending_msgs;
110 112 }
111 113 }
112 114 }
113 115 },
114 116
115 117 callbacks: function(view) {
116 118 // Create msg callbacks for a comm msg.
117 119 var callbacks = this.widget_manager.callbacks(view);
118 120
119 121 if (callbacks.iopub === undefined) {
120 122 callbacks.iopub = {};
121 123 }
122 124
123 125 var that = this;
124 126 callbacks.iopub.status = function (msg) {
125 127 that._handle_status(msg, callbacks);
126 128 };
127 129 return callbacks;
128 130 },
129 131
130 132 set: function(key, val, options) {
131 133 // Set a value.
132 134 var return_value = WidgetModel.__super__.set.apply(this, arguments);
133 135
134 136 // Backbone only remembers the diff of the most recent set()
135 137 // operation. Calling set multiple times in a row results in a
136 138 // loss of diff information. Here we keep our own running diff.
137 139 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
138 140 return return_value;
139 141 },
140 142
141 143 sync: function (method, model, options) {
142 144 // Handle sync to the back-end. Called when a model.save() is called.
143 145
144 146 // Make sure a comm exists.
145 147 var error = options.error || function() {
146 148 console.error('Backbone sync error:', arguments);
147 149 };
148 150 if (this.comm === undefined) {
149 151 error();
150 152 return false;
151 153 }
152 154
153 155 // Delete any key value pairs that the back-end already knows about.
154 156 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
155 157 if (this.state_lock !== null) {
156 158 var keys = Object.keys(this.state_lock);
157 159 for (var i=0; i<keys.length; i++) {
158 160 var key = keys[i];
159 161 if (attrs[key] === this.state_lock[key]) {
160 162 delete attrs[key];
161 163 }
162 164 }
163 165 }
164 166
165 167 // Only sync if there are attributes to send to the back-end.
166 168 attrs = this._pack_models(attrs);
167 169 if (_.size(attrs) > 0) {
168 170
169 171 // If this message was sent via backbone itself, it will not
170 172 // have any callbacks. It's important that we create callbacks
171 173 // so we can listen for status messages, etc...
172 174 var callbacks = options.callbacks || this.callbacks();
173 175
174 176 // Check throttle.
175 177 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
176 178 // The throttle has been exceeded, buffer the current msg so
177 179 // it can be sent once the kernel has finished processing
178 180 // some of the existing messages.
179 181
180 182 // Combine updates if it is a 'patch' sync, otherwise replace updates
181 183 switch (method) {
182 184 case 'patch':
183 185 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
184 186 break;
185 187 case 'update':
186 188 case 'create':
187 189 this.msg_buffer = attrs;
188 190 break;
189 191 default:
190 192 error();
191 193 return false;
192 194 }
193 195 this.msg_buffer_callbacks = callbacks;
194 196
195 197 } else {
196 198 // We haven't exceeded the throttle, send the message like
197 199 // normal.
198 200 var data = {method: 'backbone', sync_data: attrs};
199 201 this.comm.send(data, callbacks);
200 202 this.pending_msgs++;
201 203 }
202 204 }
203 205 // Since the comm is a one-way communication, assume the message
204 206 // arrived. Don't call success since we don't have a model back from the server
205 207 // this means we miss out on the 'sync' event.
206 208 this._buffered_state_diff = {};
207 209 },
208 210
209 211 save_changes: function(callbacks) {
210 212 // Push this model's state to the back-end
211 213 //
212 214 // This invokes a Backbone.Sync.
213 215 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
214 216 },
215 217
216 218 _pack_models: function(value) {
217 219 // Replace models with model ids recursively.
218 220 var that = this;
219 221 var packed;
220 222 if (value instanceof Backbone.Model) {
221 223 return "IPY_MODEL_" + value.id;
222 224
223 225 } else if ($.isArray(value)) {
224 226 packed = [];
225 227 _.each(value, function(sub_value, key) {
226 228 packed.push(that._pack_models(sub_value));
227 229 });
228 230 return packed;
229 231
230 232 } else if (value instanceof Object) {
231 233 packed = {};
232 234 _.each(value, function(sub_value, key) {
233 235 packed[key] = that._pack_models(sub_value);
234 236 });
235 237 return packed;
236 238
237 239 } else {
238 240 return value;
239 241 }
240 242 },
241 243
242 244 _unpack_models: function(value) {
243 245 // Replace model ids with models recursively.
244 246 var that = this;
245 247 var unpacked;
246 248 if ($.isArray(value)) {
247 249 unpacked = [];
248 250 _.each(value, function(sub_value, key) {
249 251 unpacked.push(that._unpack_models(sub_value));
250 252 });
251 253 return unpacked;
252 254
253 255 } else if (value instanceof Object) {
254 256 unpacked = {};
255 257 _.each(value, function(sub_value, key) {
256 258 unpacked[key] = that._unpack_models(sub_value);
257 259 });
258 260 return unpacked;
259 261
260 262 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
261 263 var model = this.widget_manager.get_model(value.slice(10, value.length));
262 264 if (model) {
263 265 return model;
264 266 } else {
265 267 return value;
266 268 }
267 269 } else {
268 270 return value;
269 271 }
270 272 },
271 273
272 274 on_some_change: function(keys, callback, context) {
273 275 // on_some_change(["key1", "key2"], foo, context) differs from
274 276 // on("change:key1 change:key2", foo, context).
275 277 // If the widget attributes key1 and key2 are both modified,
276 278 // the second form will result in foo being called twice
277 279 // while the first will call foo only once.
278 280 this.on('change', function() {
279 281 if (keys.some(this.hasChanged, this)) {
280 282 callback.apply(context);
281 283 }
282 284 }, this);
283 285
284 286 },
285 287 });
286 288 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
287 289
288 290
289 291 var WidgetView = Backbone.View.extend({
290 292 initialize: function(parameters) {
291 293 // Public constructor.
292 294 this.model.on('change',this.update,this);
293 295 this.options = parameters.options;
294 296 this.child_model_views = {};
295 297 this.child_views = {};
296 this.model.views.push(this);
297 298 this.id = this.id || IPython.utils.uuid();
299 this.model.views[this.id] = this;
298 300 this.on('displayed', function() {
299 301 this.is_displayed = true;
300 302 }, this);
301 303 },
302 304
303 305 update: function(){
304 306 // Triggered on model change.
305 307 //
306 308 // Update view to be consistent with this.model
307 309 },
308 310
309 311 create_child_view: function(child_model, options) {
310 312 // Create and return a child view.
311 313 //
312 314 // -given a model and (optionally) a view name if the view name is
313 315 // not given, it defaults to the model's default view attribute.
314 316
315 317 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
316 318 // it would be great to have the widget manager add the cell metadata
317 319 // to the subview without having to add it here.
318 320 options = $.extend({ parent: this }, options || {});
319 321 var child_view = this.model.widget_manager.create_view(child_model, options, this);
320 322
321 323 // Associate the view id with the model id.
322 324 if (this.child_model_views[child_model.id] === undefined) {
323 325 this.child_model_views[child_model.id] = [];
324 326 }
325 327 this.child_model_views[child_model.id].push(child_view.id);
326 328
327 329 // Remember the view by id.
328 330 this.child_views[child_view.id] = child_view;
329 331 return child_view;
330 332 },
331 333
332 334 pop_child_view: function(child_model) {
333 335 // Delete a child view that was previously created using create_child_view.
334 336 var view_ids = this.child_model_views[child_model.id];
335 337 if (view_ids !== undefined) {
336 338
337 339 // Only delete the first view in the list.
338 340 var view_id = view_ids[0];
339 341 var view = this.child_views[view_id];
340 342 delete this.child_views[view_id];
341 343 view_ids.splice(0,1);
342 child_model.views.pop(view);
344 delete child_model.views[view_id];
343 345
344 346 // Remove the view list specific to this model if it is empty.
345 347 if (view_ids.length === 0) {
346 348 delete this.child_model_views[child_model.id];
347 349 }
348 350 return view;
349 351 }
350 352 return null;
351 353 },
352 354
353 355 do_diff: function(old_list, new_list, removed_callback, added_callback) {
354 356 // Difference a changed list and call remove and add callbacks for
355 357 // each removed and added item in the new list.
356 358 //
357 359 // Parameters
358 360 // ----------
359 361 // old_list : array
360 362 // new_list : array
361 363 // removed_callback : Callback(item)
362 364 // Callback that is called for each item removed.
363 365 // added_callback : Callback(item)
364 366 // Callback that is called for each item added.
365 367
366 368 // Walk the lists until an unequal entry is found.
367 369 var i;
368 370 for (i = 0; i < new_list.length; i++) {
369 371 if (i >= old_list.length || new_list[i] !== old_list[i]) {
370 372 break;
371 373 }
372 374 }
373 375
374 376 // Remove the non-matching items from the old list.
375 377 for (var j = i; j < old_list.length; j++) {
376 378 removed_callback(old_list[j]);
377 379 }
378 380
379 381 // Add the rest of the new list items.
380 382 for (; i < new_list.length; i++) {
381 383 added_callback(new_list[i]);
382 384 }
383 385 },
384 386
385 387 callbacks: function(){
386 388 // Create msg callbacks for a comm msg.
387 389 return this.model.callbacks(this);
388 390 },
389 391
390 392 render: function(){
391 393 // Render the view.
392 394 //
393 395 // By default, this is only called the first time the view is created
394 396 },
395 397
396 398 show: function(){
397 399 // Show the widget-area
398 400 if (this.options && this.options.cell &&
399 401 this.options.cell.widget_area !== undefined) {
400 402 this.options.cell.widget_area.show();
401 403 }
402 404 },
403 405
404 406 send: function (content) {
405 407 // Send a custom msg associated with this view.
406 408 this.model.send(content, this.callbacks());
407 409 },
408 410
409 411 touch: function () {
410 412 this.model.save_changes(this.callbacks());
411 413 },
412 414
413 415 after_displayed: function (callback, context) {
414 416 // Calls the callback right away is the view is already displayed
415 417 // otherwise, register the callback to the 'displayed' event.
416 418 if (this.is_displayed) {
417 419 callback.apply(context);
418 420 } else {
419 421 this.on('displayed', callback, context);
420 422 }
421 423 },
422 424 });
423 425
424 426
425 427 var DOMWidgetView = WidgetView.extend({
426 428 initialize: function (parameters) {
427 429 // Public constructor
428 430 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
429 431 this.on('displayed', this.show, this);
430 432 this.model.on('change:visible', this.update_visible, this);
431 433 this.model.on('change:_css', this.update_css, this);
432 434
433 435 this.model.on('change:_dom_classes', function(model, new_classes) {
434 436 var old_classes = model.previous('_dom_classes');
435 437 this.update_classes(old_classes, new_classes);
436 438 }, this);
437 439
438 440 this.model.on('change:color', function (model, value) {
439 441 this.update_attr('color', value); }, this);
440 442
441 443 this.model.on('change:background_color', function (model, value) {
442 444 this.update_attr('background', value); }, this);
443 445
444 446 this.model.on('change:width', function (model, value) {
445 447 this.update_attr('width', value); }, this);
446 448
447 449 this.model.on('change:height', function (model, value) {
448 450 this.update_attr('height', value); }, this);
449 451
450 452 this.model.on('change:border_color', function (model, value) {
451 453 this.update_attr('border-color', value); }, this);
452 454
453 455 this.model.on('change:border_width', function (model, value) {
454 456 this.update_attr('border-width', value); }, this);
455 457
456 458 this.model.on('change:border_style', function (model, value) {
457 459 this.update_attr('border-style', value); }, this);
458 460
459 461 this.model.on('change:font_style', function (model, value) {
460 462 this.update_attr('font-style', value); }, this);
461 463
462 464 this.model.on('change:font_weight', function (model, value) {
463 465 this.update_attr('font-weight', value); }, this);
464 466
465 467 this.model.on('change:font_size', function (model, value) {
466 468 this.update_attr('font-size', this._default_px(value)); }, this);
467 469
468 470 this.model.on('change:font_family', function (model, value) {
469 471 this.update_attr('font-family', value); }, this);
470 472
471 473 this.model.on('change:padding', function (model, value) {
472 474 this.update_attr('padding', value); }, this);
473 475
474 476 this.model.on('change:margin', function (model, value) {
475 477 this.update_attr('margin', this._default_px(value)); }, this);
476 478
477 479 this.model.on('change:border_radius', function (model, value) {
478 480 this.update_attr('border-radius', this._default_px(value)); }, this);
479 481
480 482 this.after_displayed(function() {
481 483 this.update_visible(this.model, this.model.get("visible"));
482 484 this.update_css(this.model, this.model.get("_css"));
483 485
484 486 this.update_classes([], this.model.get('_dom_classes'));
485 487 this.update_attr('color', this.model.get('color'));
486 488 this.update_attr('background', this.model.get('background_color'));
487 489 this.update_attr('width', this.model.get('width'));
488 490 this.update_attr('height', this.model.get('height'));
489 491 this.update_attr('border-color', this.model.get('border_color'));
490 492 this.update_attr('border-width', this.model.get('border_width'));
491 493 this.update_attr('border-style', this.model.get('border_style'));
492 494 this.update_attr('font-style', this.model.get('font_style'));
493 495 this.update_attr('font-weight', this.model.get('font_weight'));
494 496 this.update_attr('font-size', this.model.get('font_size'));
495 497 this.update_attr('font-family', this.model.get('font_family'));
496 498 this.update_attr('padding', this.model.get('padding'));
497 499 this.update_attr('margin', this.model.get('margin'));
498 500 this.update_attr('border-radius', this.model.get('border_radius'));
499 501 }, this);
500 502 },
501 503
502 504 _default_px: function(value) {
503 505 // Makes browser interpret a numerical string as a pixel value.
504 506 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
505 507 return value.trim() + 'px';
506 508 }
507 509 return value;
508 510 },
509 511
510 512 update_attr: function(name, value) {
511 513 // Set a css attr of the widget view.
512 514 this.$el.css(name, value);
513 515 },
514 516
515 517 update_visible: function(model, value) {
516 518 // Update visibility
517 519 this.$el.toggle(value);
518 520 },
519 521
520 522 update_css: function (model, css) {
521 523 // Update the css styling of this view.
522 524 var e = this.$el;
523 525 if (css === undefined) {return;}
524 526 for (var i = 0; i < css.length; i++) {
525 527 // Apply the css traits to all elements that match the selector.
526 528 var selector = css[i][0];
527 529 var elements = this._get_selector_element(selector);
528 530 if (elements.length > 0) {
529 531 var trait_key = css[i][1];
530 532 var trait_value = css[i][2];
531 533 elements.css(trait_key ,trait_value);
532 534 }
533 535 }
534 536 },
535 537
536 538 update_classes: function (old_classes, new_classes, $el) {
537 539 // Update the DOM classes applied to an element, default to this.$el.
538 540 if ($el===undefined) {
539 541 $el = this.$el;
540 542 }
541 543 this.do_diff(old_classes, new_classes, function(removed) {
542 544 $el.removeClass(removed);
543 545 }, function(added) {
544 546 $el.addClass(added);
545 547 });
546 548 },
547 549
548 550 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
549 551 // Update the DOM classes applied to the widget based on a single
550 552 // trait's value.
551 553 //
552 554 // Given a trait value classes map, this function automatically
553 555 // handles applying the appropriate classes to the widget element
554 556 // and removing classes that are no longer valid.
555 557 //
556 558 // Parameters
557 559 // ----------
558 560 // class_map: dictionary
559 561 // Dictionary of trait values to class lists.
560 562 // Example:
561 563 // {
562 564 // success: ['alert', 'alert-success'],
563 565 // info: ['alert', 'alert-info'],
564 566 // warning: ['alert', 'alert-warning'],
565 567 // danger: ['alert', 'alert-danger']
566 568 // };
567 569 // trait_name: string
568 570 // Name of the trait to check the value of.
569 571 // previous_trait_value: optional string, default ''
570 572 // Last trait value
571 573 // $el: optional jQuery element handle, defaults to this.$el
572 574 // Element that the classes are applied to.
573 575 var key = previous_trait_value;
574 576 if (key === undefined) {
575 577 key = this.model.previous(trait_name);
576 578 }
577 579 var old_classes = class_map[key] ? class_map[key] : [];
578 580 key = this.model.get(trait_name);
579 581 var new_classes = class_map[key] ? class_map[key] : [];
580 582
581 583 this.update_classes(old_classes, new_classes, $el || this.$el);
582 584 },
583 585
584 586 _get_selector_element: function (selector) {
585 587 // Get the elements via the css selector.
586 588 var elements;
587 589 if (!selector) {
588 590 elements = this.$el;
589 591 } else {
590 592 elements = this.$el.find(selector).addBack(selector);
591 593 }
592 594 return elements;
593 595 },
594 596 });
595 597
596 598
597 599 var widget = {
598 600 'WidgetModel': WidgetModel,
599 601 'WidgetView': WidgetView,
600 602 'DOMWidgetView': DOMWidgetView,
601 603 };
602 604
603 605 // For backwards compatability.
604 606 $.extend(IPython, widget);
605 607
606 608 return widget;
607 609 });
General Comments 0
You need to be logged in to leave comments. Login now