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