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