##// END OF EJS Templates
Make sure comm-dead is applied to the top widget view class...
Jonathan Frederic -
Show More
@@ -1,742 +1,760 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/utils",
9 9 "base/js/namespace",
10 10 ], function(widgetmanager, _, Backbone, $, utils, IPython){
11 11
12 12 var WidgetModel = Backbone.Model.extend({
13 13 constructor: function (widget_manager, model_id, comm) {
14 14 /**
15 15 * Constructor
16 16 *
17 17 * Creates a WidgetModel instance.
18 18 *
19 19 * Parameters
20 20 * ----------
21 21 * widget_manager : WidgetManager instance
22 22 * model_id : string
23 23 * An ID unique to this model.
24 24 * comm : Comm instance (optional)
25 25 */
26 26 this.widget_manager = widget_manager;
27 27 this.state_change = Promise.resolve();
28 28 this._buffered_state_diff = {};
29 29 this.pending_msgs = 0;
30 30 this.msg_buffer = null;
31 31 this.state_lock = null;
32 32 this.id = model_id;
33 33 this.views = {};
34 34 this._resolve_received_state = {};
35 35
36 36 if (comm !== undefined) {
37 37 // Remember comm associated with the model.
38 38 this.comm = comm;
39 39 comm.model = this;
40 40
41 41 // Hook comm messages up to model.
42 42 comm.on_close($.proxy(this._handle_comm_closed, this));
43 43 comm.on_msg($.proxy(this._handle_comm_msg, this));
44 44
45 45 // Assume the comm is alive.
46 46 this.set_comm_live(true);
47 47 } else {
48 48 this.set_comm_live(false);
49 49 }
50
51 // Listen for the events that lead to the websocket being terminated.
52 var that = this;
53 var died = function() {
54 that.set_comm_live(false);
55 };
56 widget_manager.notebook.events.on('kernel_disconnected.Kernel', died);
57 widget_manager.notebook.events.on('kernel_killed.Kernel', died);
58 widget_manager.notebook.events.on('kernel_restarting.Kernel', died);
59 widget_manager.notebook.events.on('kernel_dead.Kernel', died);
60
50 61 return Backbone.Model.apply(this);
51 62 },
52 63
53 64 send: function (content, callbacks) {
54 65 /**
55 66 * Send a custom msg over the comm.
56 67 */
57 68 if (this.comm !== undefined) {
58 69 var data = {method: 'custom', content: content};
59 70 this.comm.send(data, callbacks);
60 71 this.pending_msgs++;
61 72 }
62 73 },
63 74
64 75 request_state: function(callbacks) {
65 76 /**
66 77 * Request a state push from the back-end.
67 78 */
68 79 if (!this.comm) {
69 80 console.error("Could not request_state because comm doesn't exist!");
70 81 return;
71 82 }
72 83
73 84 var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
74 85
75 86 // Promise that is resolved when a state is received
76 87 // from the back-end.
77 88 var that = this;
78 89 var received_state = new Promise(function(resolve) {
79 90 that._resolve_received_state[msg_id] = resolve;
80 91 });
81 92 return received_state;
82 93 },
83 94
84 95 set_comm_live: function(live) {
85 96 /**
86 97 * Change the comm_live state of the model.
87 98 */
88 99 if (this.comm_live === undefined || this.comm_live != live) {
89 100 this.comm_live = live;
90 101 this.trigger(live ? 'comm:live' : 'comm:dead', {model: this});
91 102 }
92 103 },
93 104
94 105 close: function(comm_closed) {
95 106 /**
96 107 * Close model
97 108 */
98 109 if (this.comm && !comm_closed) {
99 110 this.comm.close();
100 111 }
101 112 this.stopListening();
102 113 this.trigger('destroy', this);
103 114 delete this.comm.model; // Delete ref so GC will collect widget model.
104 115 delete this.comm;
105 116 delete this.model_id; // Delete id from model so widget manager cleans up.
106 117 _.each(this.views, function(v, id, views) {
107 118 v.then(function(view) {
108 119 view.remove();
109 120 delete views[id];
110 121 });
111 122 });
112 123 },
113 124
114 125 _handle_comm_closed: function (msg) {
115 126 /**
116 127 * Handle when a widget is closed.
117 128 */
118 129 this.trigger('comm:close');
119 130 this.close(true);
120 131 },
121 132
122 133 _handle_comm_msg: function (msg) {
123 134 /**
124 135 * Handle incoming comm msg.
125 136 */
126 137 var method = msg.content.data.method;
127 138 var that = this;
128 139 switch (method) {
129 140 case 'update':
130 141 this.state_change = this.state_change
131 142 .then(function() {
132 143 return that.set_state(msg.content.data.state);
133 144 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
134 145 .then(function() {
135 146 var parent_id = msg.parent_header.msg_id;
136 147 if (that._resolve_received_state[parent_id] !== undefined) {
137 148 that._resolve_received_state[parent_id].call();
138 149 delete that._resolve_received_state[parent_id];
139 150 }
140 151 }).catch(utils.reject("Couldn't resolve state request promise", true));
141 152 break;
142 153 case 'custom':
143 154 this.trigger('msg:custom', msg.content.data.content);
144 155 break;
145 156 case 'display':
146 157 this.widget_manager.display_view(msg, this)
147 158 .catch(utils.reject('Could not process display view msg', true));
148 159 break;
149 160 }
150 161 },
151 162
152 163 set_state: function (state) {
153 164 var that = this;
154 165 // Handle when a widget is updated via the python side.
155 166 return this._unpack_models(state).then(function(state) {
156 167 that.state_lock = state;
157 168 try {
158 169 WidgetModel.__super__.set.call(that, state);
159 170 } finally {
160 171 that.state_lock = null;
161 172 }
162 173 }).catch(utils.reject("Couldn't set model state", true));
163 174 },
164 175
165 176 get_state: function() {
166 177 // Get the serializable state of the model.
167 178 state = this.toJSON();
168 179 for (var key in state) {
169 180 if (state.hasOwnProperty(key)) {
170 181 state[key] = this._pack_models(state[key]);
171 182 }
172 183 }
173 184 return state;
174 185 },
175 186
176 187 _handle_status: function (msg, callbacks) {
177 188 /**
178 189 * Handle status msgs.
179 190 *
180 191 * execution_state : ('busy', 'idle', 'starting')
181 192 */
182 193 if (this.comm !== undefined) {
183 194 if (msg.content.execution_state ==='idle') {
184 195 // Send buffer if this message caused another message to be
185 196 // throttled.
186 197 if (this.msg_buffer !== null &&
187 198 (this.get('msg_throttle') || 3) === this.pending_msgs) {
188 199 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
189 200 this.comm.send(data, callbacks);
190 201 this.msg_buffer = null;
191 202 } else {
192 203 --this.pending_msgs;
193 204 }
194 205 }
195 206 }
196 207 },
197 208
198 209 callbacks: function(view) {
199 210 /**
200 211 * Create msg callbacks for a comm msg.
201 212 */
202 213 var callbacks = this.widget_manager.callbacks(view);
203 214
204 215 if (callbacks.iopub === undefined) {
205 216 callbacks.iopub = {};
206 217 }
207 218
208 219 var that = this;
209 220 callbacks.iopub.status = function (msg) {
210 221 that._handle_status(msg, callbacks);
211 222 };
212 223 return callbacks;
213 224 },
214 225
215 226 set: function(key, val, options) {
216 227 /**
217 228 * Set a value.
218 229 */
219 230 var return_value = WidgetModel.__super__.set.apply(this, arguments);
220 231
221 232 // Backbone only remembers the diff of the most recent set()
222 233 // operation. Calling set multiple times in a row results in a
223 234 // loss of diff information. Here we keep our own running diff.
224 235 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
225 236 return return_value;
226 237 },
227 238
228 239 sync: function (method, model, options) {
229 240 /**
230 241 * Handle sync to the back-end. Called when a model.save() is called.
231 242 *
232 243 * Make sure a comm exists.
233 244 */
234 245 var error = options.error || function() {
235 246 console.error('Backbone sync error:', arguments);
236 247 };
237 248 if (this.comm === undefined) {
238 249 error();
239 250 return false;
240 251 }
241 252
242 253 // Delete any key value pairs that the back-end already knows about.
243 254 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
244 255 if (this.state_lock !== null) {
245 256 var keys = Object.keys(this.state_lock);
246 257 for (var i=0; i<keys.length; i++) {
247 258 var key = keys[i];
248 259 if (attrs[key] === this.state_lock[key]) {
249 260 delete attrs[key];
250 261 }
251 262 }
252 263 }
253 264
254 265 // Only sync if there are attributes to send to the back-end.
255 266 attrs = this._pack_models(attrs);
256 267 if (_.size(attrs) > 0) {
257 268
258 269 // If this message was sent via backbone itself, it will not
259 270 // have any callbacks. It's important that we create callbacks
260 271 // so we can listen for status messages, etc...
261 272 var callbacks = options.callbacks || this.callbacks();
262 273
263 274 // Check throttle.
264 275 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
265 276 // The throttle has been exceeded, buffer the current msg so
266 277 // it can be sent once the kernel has finished processing
267 278 // some of the existing messages.
268 279
269 280 // Combine updates if it is a 'patch' sync, otherwise replace updates
270 281 switch (method) {
271 282 case 'patch':
272 283 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
273 284 break;
274 285 case 'update':
275 286 case 'create':
276 287 this.msg_buffer = attrs;
277 288 break;
278 289 default:
279 290 error();
280 291 return false;
281 292 }
282 293 this.msg_buffer_callbacks = callbacks;
283 294
284 295 } else {
285 296 // We haven't exceeded the throttle, send the message like
286 297 // normal.
287 298 var data = {method: 'backbone', sync_data: attrs};
288 299 this.comm.send(data, callbacks);
289 300 this.pending_msgs++;
290 301 }
291 302 }
292 303 // Since the comm is a one-way communication, assume the message
293 304 // arrived. Don't call success since we don't have a model back from the server
294 305 // this means we miss out on the 'sync' event.
295 306 this._buffered_state_diff = {};
296 307 },
297 308
298 309 save_changes: function(callbacks) {
299 310 /**
300 311 * Push this model's state to the back-end
301 312 *
302 313 * This invokes a Backbone.Sync.
303 314 */
304 315 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
305 316 },
306 317
307 318 _pack_models: function(value) {
308 319 /**
309 320 * Replace models with model ids recursively.
310 321 */
311 322 var that = this;
312 323 var packed;
313 324 if (value instanceof Backbone.Model) {
314 325 return "IPY_MODEL_" + value.id;
315 326
316 327 } else if ($.isArray(value)) {
317 328 packed = [];
318 329 _.each(value, function(sub_value, key) {
319 330 packed.push(that._pack_models(sub_value));
320 331 });
321 332 return packed;
322 333 } else if (value instanceof Date || value instanceof String) {
323 334 return value;
324 335 } else if (value instanceof Object) {
325 336 packed = {};
326 337 _.each(value, function(sub_value, key) {
327 338 packed[key] = that._pack_models(sub_value);
328 339 });
329 340 return packed;
330 341
331 342 } else {
332 343 return value;
333 344 }
334 345 },
335 346
336 347 _unpack_models: function(value) {
337 348 /**
338 349 * Replace model ids with models recursively.
339 350 */
340 351 var that = this;
341 352 var unpacked;
342 353 if ($.isArray(value)) {
343 354 unpacked = [];
344 355 _.each(value, function(sub_value, key) {
345 356 unpacked.push(that._unpack_models(sub_value));
346 357 });
347 358 return Promise.all(unpacked);
348 359 } else if (value instanceof Object) {
349 360 unpacked = {};
350 361 _.each(value, function(sub_value, key) {
351 362 unpacked[key] = that._unpack_models(sub_value);
352 363 });
353 364 return utils.resolve_promises_dict(unpacked);
354 365 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
355 366 // get_model returns a promise already
356 367 return this.widget_manager.get_model(value.slice(10, value.length));
357 368 } else {
358 369 return Promise.resolve(value);
359 370 }
360 371 },
361 372
362 373 on_some_change: function(keys, callback, context) {
363 374 /**
364 375 * on_some_change(["key1", "key2"], foo, context) differs from
365 376 * on("change:key1 change:key2", foo, context).
366 377 * If the widget attributes key1 and key2 are both modified,
367 378 * the second form will result in foo being called twice
368 379 * while the first will call foo only once.
369 380 */
370 381 this.on('change', function() {
371 382 if (keys.some(this.hasChanged, this)) {
372 383 callback.apply(context);
373 384 }
374 385 }, this);
375 386
376 387 },
377 388 });
378 389 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
379 390
380 391
381 392 var WidgetView = Backbone.View.extend({
382 393 initialize: function(parameters) {
383 394 /**
384 395 * Public constructor.
385 396 */
386 397 this.model.on('change',this.update,this);
398 this.model.on('comm:live', function() {
399 this.$el.removeClass('comm-dead');
400 }, this);
401 this.model.on('comm:dead', function() {
402 this.$el.addClass('comm-dead');
403 }, this);
404
387 405 this.options = parameters.options;
388 406 this.on('displayed', function() {
389 407 this.is_displayed = true;
390 408 }, this);
391 409 },
392 410
393 411 update: function(){
394 412 /**
395 413 * Triggered on model change.
396 414 *
397 415 * Update view to be consistent with this.model
398 416 */
399 417 },
400 418
401 419 create_child_view: function(child_model, options) {
402 420 /**
403 421 * Create and promise that resolves to a child view of a given model
404 422 */
405 423 var that = this;
406 424 options = $.extend({ parent: this }, options || {});
407 425 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
408 426 },
409 427
410 428 callbacks: function(){
411 429 /**
412 430 * Create msg callbacks for a comm msg.
413 431 */
414 432 return this.model.callbacks(this);
415 433 },
416 434
417 435 render: function(){
418 436 /**
419 437 * Render the view.
420 438 *
421 439 * By default, this is only called the first time the view is created
422 440 */
423 441 },
424 442
425 443 send: function (content) {
426 444 /**
427 445 * Send a custom msg associated with this view.
428 446 */
429 447 this.model.send(content, this.callbacks());
430 448 },
431 449
432 450 touch: function () {
433 451 this.model.save_changes(this.callbacks());
434 452 },
435 453
436 454 after_displayed: function (callback, context) {
437 455 /**
438 456 * Calls the callback right away is the view is already displayed
439 457 * otherwise, register the callback to the 'displayed' event.
440 458 */
441 459 if (this.is_displayed) {
442 460 callback.apply(context);
443 461 } else {
444 462 this.on('displayed', callback, context);
445 463 }
446 464 },
447 465
448 466 remove: function () {
449 467 // Raise a remove event when the view is removed.
450 468 WidgetView.__super__.remove.apply(this, arguments);
451 469 this.trigger('remove');
452 470 }
453 471 });
454 472
455 473
456 474 var DOMWidgetView = WidgetView.extend({
457 475 initialize: function (parameters) {
458 476 /**
459 477 * Public constructor
460 478 */
461 479 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
462 480 this.model.on('change:visible', this.update_visible, this);
463 481 this.model.on('change:_css', this.update_css, this);
464 482
465 483 this.model.on('change:_dom_classes', function(model, new_classes) {
466 484 var old_classes = model.previous('_dom_classes');
467 485 this.update_classes(old_classes, new_classes);
468 486 }, this);
469 487
470 488 this.model.on('change:color', function (model, value) {
471 489 this.update_attr('color', value); }, this);
472 490
473 491 this.model.on('change:background_color', function (model, value) {
474 492 this.update_attr('background', value); }, this);
475 493
476 494 this.model.on('change:width', function (model, value) {
477 495 this.update_attr('width', value); }, this);
478 496
479 497 this.model.on('change:height', function (model, value) {
480 498 this.update_attr('height', value); }, this);
481 499
482 500 this.model.on('change:border_color', function (model, value) {
483 501 this.update_attr('border-color', value); }, this);
484 502
485 503 this.model.on('change:border_width', function (model, value) {
486 504 this.update_attr('border-width', value); }, this);
487 505
488 506 this.model.on('change:border_style', function (model, value) {
489 507 this.update_attr('border-style', value); }, this);
490 508
491 509 this.model.on('change:font_style', function (model, value) {
492 510 this.update_attr('font-style', value); }, this);
493 511
494 512 this.model.on('change:font_weight', function (model, value) {
495 513 this.update_attr('font-weight', value); }, this);
496 514
497 515 this.model.on('change:font_size', function (model, value) {
498 516 this.update_attr('font-size', this._default_px(value)); }, this);
499 517
500 518 this.model.on('change:font_family', function (model, value) {
501 519 this.update_attr('font-family', value); }, this);
502 520
503 521 this.model.on('change:padding', function (model, value) {
504 522 this.update_attr('padding', value); }, this);
505 523
506 524 this.model.on('change:margin', function (model, value) {
507 525 this.update_attr('margin', this._default_px(value)); }, this);
508 526
509 527 this.model.on('change:border_radius', function (model, value) {
510 528 this.update_attr('border-radius', this._default_px(value)); }, this);
511 529
512 530 this.after_displayed(function() {
513 531 this.update_visible(this.model, this.model.get("visible"));
514 532 this.update_classes([], this.model.get('_dom_classes'));
515 533
516 534 this.update_attr('color', this.model.get('color'));
517 535 this.update_attr('background', this.model.get('background_color'));
518 536 this.update_attr('width', this.model.get('width'));
519 537 this.update_attr('height', this.model.get('height'));
520 538 this.update_attr('border-color', this.model.get('border_color'));
521 539 this.update_attr('border-width', this.model.get('border_width'));
522 540 this.update_attr('border-style', this.model.get('border_style'));
523 541 this.update_attr('font-style', this.model.get('font_style'));
524 542 this.update_attr('font-weight', this.model.get('font_weight'));
525 543 this.update_attr('font-size', this.model.get('font_size'));
526 544 this.update_attr('font-family', this.model.get('font_family'));
527 545 this.update_attr('padding', this.model.get('padding'));
528 546 this.update_attr('margin', this.model.get('margin'));
529 547 this.update_attr('border-radius', this.model.get('border_radius'));
530 548
531 549 this.update_css(this.model, this.model.get("_css"));
532 550 }, this);
533 551 },
534 552
535 553 _default_px: function(value) {
536 554 /**
537 555 * Makes browser interpret a numerical string as a pixel value.
538 556 */
539 557 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
540 558 return value.trim() + 'px';
541 559 }
542 560 return value;
543 561 },
544 562
545 563 update_attr: function(name, value) {
546 564 /**
547 565 * Set a css attr of the widget view.
548 566 */
549 567 this.$el.css(name, value);
550 568 },
551 569
552 570 update_visible: function(model, value) {
553 571 /**
554 572 * Update visibility
555 573 */
556 574 switch(value) {
557 575 case null: // python None
558 576 this.$el.show().css('visibility', 'hidden'); break;
559 577 case false:
560 578 this.$el.hide(); break;
561 579 case true:
562 580 this.$el.show().css('visibility', ''); break;
563 581 }
564 582 },
565 583
566 584 update_css: function (model, css) {
567 585 /**
568 586 * Update the css styling of this view.
569 587 */
570 588 var e = this.$el;
571 589 if (css === undefined) {return;}
572 590 for (var i = 0; i < css.length; i++) {
573 591 // Apply the css traits to all elements that match the selector.
574 592 var selector = css[i][0];
575 593 var elements = this._get_selector_element(selector);
576 594 if (elements.length > 0) {
577 595 var trait_key = css[i][1];
578 596 var trait_value = css[i][2];
579 597 elements.css(trait_key ,trait_value);
580 598 }
581 599 }
582 600 },
583 601
584 602 update_classes: function (old_classes, new_classes, $el) {
585 603 /**
586 604 * Update the DOM classes applied to an element, default to this.$el.
587 605 */
588 606 if ($el===undefined) {
589 607 $el = this.$el;
590 608 }
591 609 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
592 610 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
593 611 },
594 612
595 613 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
596 614 /**
597 615 * Update the DOM classes applied to the widget based on a single
598 616 * trait's value.
599 617 *
600 618 * Given a trait value classes map, this function automatically
601 619 * handles applying the appropriate classes to the widget element
602 620 * and removing classes that are no longer valid.
603 621 *
604 622 * Parameters
605 623 * ----------
606 624 * class_map: dictionary
607 625 * Dictionary of trait values to class lists.
608 626 * Example:
609 627 * {
610 628 * success: ['alert', 'alert-success'],
611 629 * info: ['alert', 'alert-info'],
612 630 * warning: ['alert', 'alert-warning'],
613 631 * danger: ['alert', 'alert-danger']
614 632 * };
615 633 * trait_name: string
616 634 * Name of the trait to check the value of.
617 635 * previous_trait_value: optional string, default ''
618 636 * Last trait value
619 637 * $el: optional jQuery element handle, defaults to this.$el
620 638 * Element that the classes are applied to.
621 639 */
622 640 var key = previous_trait_value;
623 641 if (key === undefined) {
624 642 key = this.model.previous(trait_name);
625 643 }
626 644 var old_classes = class_map[key] ? class_map[key] : [];
627 645 key = this.model.get(trait_name);
628 646 var new_classes = class_map[key] ? class_map[key] : [];
629 647
630 648 this.update_classes(old_classes, new_classes, $el || this.$el);
631 649 },
632 650
633 651 _get_selector_element: function (selector) {
634 652 /**
635 653 * Get the elements via the css selector.
636 654 */
637 655 var elements;
638 656 if (!selector) {
639 657 elements = this.$el;
640 658 } else {
641 659 elements = this.$el.find(selector).addBack(selector);
642 660 }
643 661 return elements;
644 662 },
645 663
646 664 typeset: function(element, text){
647 665 utils.typeset.apply(null, arguments);
648 666 },
649 667 });
650 668
651 669
652 670 var ViewList = function(create_view, remove_view, context) {
653 671 /**
654 672 * - create_view and remove_view are default functions called when adding or removing views
655 673 * - create_view takes a model and returns a view or a promise for a view for that model
656 674 * - remove_view takes a view and destroys it (including calling `view.remove()`)
657 675 * - each time the update() function is called with a new list, the create and remove
658 676 * callbacks will be called in an order so that if you append the views created in the
659 677 * create callback and remove the views in the remove callback, you will duplicate
660 678 * the order of the list.
661 679 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
662 680 * - the context defaults to the created ViewList. If you pass another context, the create and remove
663 681 * will be called in that context.
664 682 */
665 683
666 684 this.initialize.apply(this, arguments);
667 685 };
668 686
669 687 _.extend(ViewList.prototype, {
670 688 initialize: function(create_view, remove_view, context) {
671 689 this._handler_context = context || this;
672 690 this._models = [];
673 691 this.views = []; // list of promises for views
674 692 this._create_view = create_view;
675 693 this._remove_view = remove_view || function(view) {view.remove();};
676 694 },
677 695
678 696 update: function(new_models, create_view, remove_view, context) {
679 697 /**
680 698 * the create_view, remove_view, and context arguments override the defaults
681 699 * specified when the list is created.
682 700 * after this function, the .views attribute is a list of promises for views
683 701 * if you want to perform some action on the list of views, do something like
684 702 * `Promise.all(myviewlist.views).then(function(views) {...});`
685 703 */
686 704 var remove = remove_view || this._remove_view;
687 705 var create = create_view || this._create_view;
688 706 var context = context || this._handler_context;
689 707 var i = 0;
690 708 // first, skip past the beginning of the lists if they are identical
691 709 for (; i < new_models.length; i++) {
692 710 if (i >= this._models.length || new_models[i] !== this._models[i]) {
693 711 break;
694 712 }
695 713 }
696 714
697 715 var first_removed = i;
698 716 // Remove the non-matching items from the old list.
699 717 var removed = this.views.splice(first_removed, this.views.length-first_removed);
700 718 for (var j = 0; j < removed.length; j++) {
701 719 removed[j].then(function(view) {
702 720 remove.call(context, view)
703 721 });
704 722 }
705 723
706 724 // Add the rest of the new list items.
707 725 for (; i < new_models.length; i++) {
708 726 this.views.push(Promise.resolve(create.call(context, new_models[i])));
709 727 }
710 728 // make a copy of the input array
711 729 this._models = new_models.slice();
712 730 },
713 731
714 732 remove: function() {
715 733 /**
716 734 * removes every view in the list; convenience function for `.update([])`
717 735 * that should be faster
718 736 * returns a promise that resolves after this removal is done
719 737 */
720 738 var that = this;
721 739 return Promise.all(this.views).then(function(views) {
722 740 for (var i = 0; i < that.views.length; i++) {
723 741 that._remove_view.call(that._handler_context, views[i]);
724 742 }
725 743 that.views = [];
726 744 that._models = [];
727 745 });
728 746 },
729 747 });
730 748
731 749 var widget = {
732 750 'WidgetModel': WidgetModel,
733 751 'WidgetView': WidgetView,
734 752 'DOMWidgetView': DOMWidgetView,
735 753 'ViewList': ViewList,
736 754 };
737 755
738 756 // For backwards compatability.
739 757 $.extend(IPython, widget);
740 758
741 759 return widget;
742 760 });
General Comments 0
You need to be logged in to leave comments. Login now