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