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