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