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