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