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