##// END OF EJS Templates
Merge pull request #6226 from jasongrout/css-top-default...
Jonathan Frederic -
r17559:802e00b9 merge
parent child Browse files
Show More
@@ -1,494 +1,486 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.key_value_lock = null;
27 this.key_value_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 delete this.comm.model; // Delete ref so GC will collect widget model.
55 delete this.comm.model; // Delete ref so GC will collect widget model.
56 delete this.comm;
56 delete this.comm;
57 delete this.model_id; // Delete id from model so widget manager cleans up.
57 delete this.model_id; // Delete id from model so widget manager cleans up.
58 _.each(this.views, function(view, i) {
58 _.each(this.views, function(view, i) {
59 view.remove();
59 view.remove();
60 });
60 });
61 },
61 },
62
62
63 _handle_comm_msg: function (msg) {
63 _handle_comm_msg: function (msg) {
64 // Handle incoming comm msg.
64 // Handle incoming comm msg.
65 var method = msg.content.data.method;
65 var method = msg.content.data.method;
66 switch (method) {
66 switch (method) {
67 case 'update':
67 case 'update':
68 this.apply_update(msg.content.data.state);
68 this.apply_update(msg.content.data.state);
69 break;
69 break;
70 case 'custom':
70 case 'custom':
71 this.trigger('msg:custom', msg.content.data.content);
71 this.trigger('msg:custom', msg.content.data.content);
72 break;
72 break;
73 case 'display':
73 case 'display':
74 this.widget_manager.display_view(msg, this);
74 this.widget_manager.display_view(msg, this);
75 break;
75 break;
76 }
76 }
77 },
77 },
78
78
79 apply_update: function (state) {
79 apply_update: function (state) {
80 // Handle when a widget is updated via the python side.
80 // Handle when a widget is updated via the python side.
81 var that = this;
81 var that = this;
82 _.each(state, function(value, key) {
82 _.each(state, function(value, key) {
83 that.key_value_lock = [key, value];
83 that.key_value_lock = [key, value];
84 try {
84 try {
85 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
85 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
86 } finally {
86 } finally {
87 that.key_value_lock = null;
87 that.key_value_lock = null;
88 }
88 }
89 });
89 });
90 },
90 },
91
91
92 _handle_status: function (msg, callbacks) {
92 _handle_status: function (msg, callbacks) {
93 // Handle status msgs.
93 // Handle status msgs.
94
94
95 // execution_state : ('busy', 'idle', 'starting')
95 // execution_state : ('busy', 'idle', 'starting')
96 if (this.comm !== undefined) {
96 if (this.comm !== undefined) {
97 if (msg.content.execution_state ==='idle') {
97 if (msg.content.execution_state ==='idle') {
98 // Send buffer if this message caused another message to be
98 // Send buffer if this message caused another message to be
99 // throttled.
99 // throttled.
100 if (this.msg_buffer !== null &&
100 if (this.msg_buffer !== null &&
101 (this.get('msg_throttle') || 3) === this.pending_msgs) {
101 (this.get('msg_throttle') || 3) === this.pending_msgs) {
102 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
102 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
103 this.comm.send(data, callbacks);
103 this.comm.send(data, callbacks);
104 this.msg_buffer = null;
104 this.msg_buffer = null;
105 } else {
105 } else {
106 --this.pending_msgs;
106 --this.pending_msgs;
107 }
107 }
108 }
108 }
109 }
109 }
110 },
110 },
111
111
112 callbacks: function(view) {
112 callbacks: function(view) {
113 // Create msg callbacks for a comm msg.
113 // Create msg callbacks for a comm msg.
114 var callbacks = this.widget_manager.callbacks(view);
114 var callbacks = this.widget_manager.callbacks(view);
115
115
116 if (callbacks.iopub === undefined) {
116 if (callbacks.iopub === undefined) {
117 callbacks.iopub = {};
117 callbacks.iopub = {};
118 }
118 }
119
119
120 var that = this;
120 var that = this;
121 callbacks.iopub.status = function (msg) {
121 callbacks.iopub.status = function (msg) {
122 that._handle_status(msg, callbacks);
122 that._handle_status(msg, callbacks);
123 };
123 };
124 return callbacks;
124 return callbacks;
125 },
125 },
126
126
127 set: function(key, val, options) {
127 set: function(key, val, options) {
128 // Set a value.
128 // Set a value.
129 var return_value = WidgetModel.__super__.set.apply(this, arguments);
129 var return_value = WidgetModel.__super__.set.apply(this, arguments);
130
130
131 // Backbone only remembers the diff of the most recent set()
131 // Backbone only remembers the diff of the most recent set()
132 // operation. Calling set multiple times in a row results in a
132 // operation. Calling set multiple times in a row results in a
133 // loss of diff information. Here we keep our own running diff.
133 // loss of diff information. Here we keep our own running diff.
134 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
134 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
135 return return_value;
135 return return_value;
136 },
136 },
137
137
138 sync: function (method, model, options) {
138 sync: function (method, model, options) {
139 // Handle sync to the back-end. Called when a model.save() is called.
139 // Handle sync to the back-end. Called when a model.save() is called.
140
140
141 // Make sure a comm exists.
141 // Make sure a comm exists.
142 var error = options.error || function() {
142 var error = options.error || function() {
143 console.error('Backbone sync error:', arguments);
143 console.error('Backbone sync error:', arguments);
144 };
144 };
145 if (this.comm === undefined) {
145 if (this.comm === undefined) {
146 error();
146 error();
147 return false;
147 return false;
148 }
148 }
149
149
150 // Delete any key value pairs that the back-end already knows about.
150 // Delete any key value pairs that the back-end already knows about.
151 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
151 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
152 if (this.key_value_lock !== null) {
152 if (this.key_value_lock !== null) {
153 var key = this.key_value_lock[0];
153 var key = this.key_value_lock[0];
154 var value = this.key_value_lock[1];
154 var value = this.key_value_lock[1];
155 if (attrs[key] === value) {
155 if (attrs[key] === value) {
156 delete attrs[key];
156 delete attrs[key];
157 }
157 }
158 }
158 }
159
159
160 // Only sync if there are attributes to send to the back-end.
160 // Only sync if there are attributes to send to the back-end.
161 attrs = this._pack_models(attrs);
161 attrs = this._pack_models(attrs);
162 if (_.size(attrs) > 0) {
162 if (_.size(attrs) > 0) {
163
163
164 // If this message was sent via backbone itself, it will not
164 // If this message was sent via backbone itself, it will not
165 // have any callbacks. It's important that we create callbacks
165 // have any callbacks. It's important that we create callbacks
166 // so we can listen for status messages, etc...
166 // so we can listen for status messages, etc...
167 var callbacks = options.callbacks || this.callbacks();
167 var callbacks = options.callbacks || this.callbacks();
168
168
169 // Check throttle.
169 // Check throttle.
170 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
170 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
171 // The throttle has been exceeded, buffer the current msg so
171 // The throttle has been exceeded, buffer the current msg so
172 // it can be sent once the kernel has finished processing
172 // it can be sent once the kernel has finished processing
173 // some of the existing messages.
173 // some of the existing messages.
174
174
175 // Combine updates if it is a 'patch' sync, otherwise replace updates
175 // Combine updates if it is a 'patch' sync, otherwise replace updates
176 switch (method) {
176 switch (method) {
177 case 'patch':
177 case 'patch':
178 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
178 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
179 break;
179 break;
180 case 'update':
180 case 'update':
181 case 'create':
181 case 'create':
182 this.msg_buffer = attrs;
182 this.msg_buffer = attrs;
183 break;
183 break;
184 default:
184 default:
185 error();
185 error();
186 return false;
186 return false;
187 }
187 }
188 this.msg_buffer_callbacks = callbacks;
188 this.msg_buffer_callbacks = callbacks;
189
189
190 } else {
190 } else {
191 // We haven't exceeded the throttle, send the message like
191 // We haven't exceeded the throttle, send the message like
192 // normal.
192 // normal.
193 var data = {method: 'backbone', sync_data: attrs};
193 var data = {method: 'backbone', sync_data: attrs};
194 this.comm.send(data, callbacks);
194 this.comm.send(data, callbacks);
195 this.pending_msgs++;
195 this.pending_msgs++;
196 }
196 }
197 }
197 }
198 // Since the comm is a one-way communication, assume the message
198 // Since the comm is a one-way communication, assume the message
199 // arrived. Don't call success since we don't have a model back from the server
199 // arrived. Don't call success since we don't have a model back from the server
200 // this means we miss out on the 'sync' event.
200 // this means we miss out on the 'sync' event.
201 this._buffered_state_diff = {};
201 this._buffered_state_diff = {};
202 },
202 },
203
203
204 save_changes: function(callbacks) {
204 save_changes: function(callbacks) {
205 // Push this model's state to the back-end
205 // Push this model's state to the back-end
206 //
206 //
207 // This invokes a Backbone.Sync.
207 // This invokes a Backbone.Sync.
208 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
208 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
209 },
209 },
210
210
211 _pack_models: function(value) {
211 _pack_models: function(value) {
212 // Replace models with model ids recursively.
212 // Replace models with model ids recursively.
213 var that = this;
213 var that = this;
214 var packed;
214 var packed;
215 if (value instanceof Backbone.Model) {
215 if (value instanceof Backbone.Model) {
216 return "IPY_MODEL_" + value.id;
216 return "IPY_MODEL_" + value.id;
217
217
218 } else if ($.isArray(value)) {
218 } else if ($.isArray(value)) {
219 packed = [];
219 packed = [];
220 _.each(value, function(sub_value, key) {
220 _.each(value, function(sub_value, key) {
221 packed.push(that._pack_models(sub_value));
221 packed.push(that._pack_models(sub_value));
222 });
222 });
223 return packed;
223 return packed;
224
224
225 } else if (value instanceof Object) {
225 } else if (value instanceof Object) {
226 packed = {};
226 packed = {};
227 _.each(value, function(sub_value, key) {
227 _.each(value, function(sub_value, key) {
228 packed[key] = that._pack_models(sub_value);
228 packed[key] = that._pack_models(sub_value);
229 });
229 });
230 return packed;
230 return packed;
231
231
232 } else {
232 } else {
233 return value;
233 return value;
234 }
234 }
235 },
235 },
236
236
237 _unpack_models: function(value) {
237 _unpack_models: function(value) {
238 // Replace model ids with models recursively.
238 // Replace model ids with models recursively.
239 var that = this;
239 var that = this;
240 var unpacked;
240 var unpacked;
241 if ($.isArray(value)) {
241 if ($.isArray(value)) {
242 unpacked = [];
242 unpacked = [];
243 _.each(value, function(sub_value, key) {
243 _.each(value, function(sub_value, key) {
244 unpacked.push(that._unpack_models(sub_value));
244 unpacked.push(that._unpack_models(sub_value));
245 });
245 });
246 return unpacked;
246 return unpacked;
247
247
248 } else if (value instanceof Object) {
248 } else if (value instanceof Object) {
249 unpacked = {};
249 unpacked = {};
250 _.each(value, function(sub_value, key) {
250 _.each(value, function(sub_value, key) {
251 unpacked[key] = that._unpack_models(sub_value);
251 unpacked[key] = that._unpack_models(sub_value);
252 });
252 });
253 return unpacked;
253 return unpacked;
254
254
255 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
255 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
256 var model = this.widget_manager.get_model(value.slice(10, value.length));
256 var model = this.widget_manager.get_model(value.slice(10, value.length));
257 if (model) {
257 if (model) {
258 return model;
258 return model;
259 } else {
259 } else {
260 return value;
260 return value;
261 }
261 }
262 } else {
262 } else {
263 return value;
263 return value;
264 }
264 }
265 },
265 },
266
266
267 });
267 });
268 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
268 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
269
269
270
270
271 var WidgetView = Backbone.View.extend({
271 var WidgetView = Backbone.View.extend({
272 initialize: function(parameters) {
272 initialize: function(parameters) {
273 // Public constructor.
273 // Public constructor.
274 this.model.on('change',this.update,this);
274 this.model.on('change',this.update,this);
275 this.options = parameters.options;
275 this.options = parameters.options;
276 this.child_model_views = {};
276 this.child_model_views = {};
277 this.child_views = {};
277 this.child_views = {};
278 this.model.views.push(this);
278 this.model.views.push(this);
279 this.id = this.id || IPython.utils.uuid();
279 this.id = this.id || IPython.utils.uuid();
280 this.on('displayed', function() {
280 this.on('displayed', function() {
281 this.is_displayed = true;
281 this.is_displayed = true;
282 }, this);
282 }, this);
283 },
283 },
284
284
285 update: function(){
285 update: function(){
286 // Triggered on model change.
286 // Triggered on model change.
287 //
287 //
288 // Update view to be consistent with this.model
288 // Update view to be consistent with this.model
289 },
289 },
290
290
291 create_child_view: function(child_model, options) {
291 create_child_view: function(child_model, options) {
292 // Create and return a child view.
292 // Create and return a child view.
293 //
293 //
294 // -given a model and (optionally) a view name if the view name is
294 // -given a model and (optionally) a view name if the view name is
295 // not given, it defaults to the model's default view attribute.
295 // not given, it defaults to the model's default view attribute.
296
296
297 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
297 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
298 // it would be great to have the widget manager add the cell metadata
298 // it would be great to have the widget manager add the cell metadata
299 // to the subview without having to add it here.
299 // to the subview without having to add it here.
300 options = $.extend({ parent: this }, options || {});
300 options = $.extend({ parent: this }, options || {});
301 var child_view = this.model.widget_manager.create_view(child_model, options, this);
301 var child_view = this.model.widget_manager.create_view(child_model, options, this);
302
302
303 // Associate the view id with the model id.
303 // Associate the view id with the model id.
304 if (this.child_model_views[child_model.id] === undefined) {
304 if (this.child_model_views[child_model.id] === undefined) {
305 this.child_model_views[child_model.id] = [];
305 this.child_model_views[child_model.id] = [];
306 }
306 }
307 this.child_model_views[child_model.id].push(child_view.id);
307 this.child_model_views[child_model.id].push(child_view.id);
308
308
309 // Remember the view by id.
309 // Remember the view by id.
310 this.child_views[child_view.id] = child_view;
310 this.child_views[child_view.id] = child_view;
311 return child_view;
311 return child_view;
312 },
312 },
313
313
314 pop_child_view: function(child_model) {
314 pop_child_view: function(child_model) {
315 // Delete a child view that was previously created using create_child_view.
315 // Delete a child view that was previously created using create_child_view.
316 var view_ids = this.child_model_views[child_model.id];
316 var view_ids = this.child_model_views[child_model.id];
317 if (view_ids !== undefined) {
317 if (view_ids !== undefined) {
318
318
319 // Only delete the first view in the list.
319 // Only delete the first view in the list.
320 var view_id = view_ids[0];
320 var view_id = view_ids[0];
321 var view = this.child_views[view_id];
321 var view = this.child_views[view_id];
322 delete this.child_views[view_id];
322 delete this.child_views[view_id];
323 view_ids.splice(0,1);
323 view_ids.splice(0,1);
324 child_model.views.pop(view);
324 child_model.views.pop(view);
325
325
326 // Remove the view list specific to this model if it is empty.
326 // Remove the view list specific to this model if it is empty.
327 if (view_ids.length === 0) {
327 if (view_ids.length === 0) {
328 delete this.child_model_views[child_model.id];
328 delete this.child_model_views[child_model.id];
329 }
329 }
330 return view;
330 return view;
331 }
331 }
332 return null;
332 return null;
333 },
333 },
334
334
335 do_diff: function(old_list, new_list, removed_callback, added_callback) {
335 do_diff: function(old_list, new_list, removed_callback, added_callback) {
336 // Difference a changed list and call remove and add callbacks for
336 // Difference a changed list and call remove and add callbacks for
337 // each removed and added item in the new list.
337 // each removed and added item in the new list.
338 //
338 //
339 // Parameters
339 // Parameters
340 // ----------
340 // ----------
341 // old_list : array
341 // old_list : array
342 // new_list : array
342 // new_list : array
343 // removed_callback : Callback(item)
343 // removed_callback : Callback(item)
344 // Callback that is called for each item removed.
344 // Callback that is called for each item removed.
345 // added_callback : Callback(item)
345 // added_callback : Callback(item)
346 // Callback that is called for each item added.
346 // Callback that is called for each item added.
347
347
348 // Walk the lists until an unequal entry is found.
348 // Walk the lists until an unequal entry is found.
349 var i;
349 var i;
350 for (i = 0; i < new_list.length; i++) {
350 for (i = 0; i < new_list.length; i++) {
351 if (i >= old_list.length || new_list[i] !== old_list[i]) {
351 if (i >= old_list.length || new_list[i] !== old_list[i]) {
352 break;
352 break;
353 }
353 }
354 }
354 }
355
355
356 // Remove the non-matching items from the old list.
356 // Remove the non-matching items from the old list.
357 for (var j = i; j < old_list.length; j++) {
357 for (var j = i; j < old_list.length; j++) {
358 removed_callback(old_list[j]);
358 removed_callback(old_list[j]);
359 }
359 }
360
360
361 // Add the rest of the new list items.
361 // Add the rest of the new list items.
362 for (; i < new_list.length; i++) {
362 for (; i < new_list.length; i++) {
363 added_callback(new_list[i]);
363 added_callback(new_list[i]);
364 }
364 }
365 },
365 },
366
366
367 callbacks: function(){
367 callbacks: function(){
368 // Create msg callbacks for a comm msg.
368 // Create msg callbacks for a comm msg.
369 return this.model.callbacks(this);
369 return this.model.callbacks(this);
370 },
370 },
371
371
372 render: function(){
372 render: function(){
373 // Render the view.
373 // Render the view.
374 //
374 //
375 // By default, this is only called the first time the view is created
375 // By default, this is only called the first time the view is created
376 },
376 },
377
377
378 show: function(){
378 show: function(){
379 // Show the widget-area
379 // Show the widget-area
380 if (this.options && this.options.cell &&
380 if (this.options && this.options.cell &&
381 this.options.cell.widget_area !== undefined) {
381 this.options.cell.widget_area !== undefined) {
382 this.options.cell.widget_area.show();
382 this.options.cell.widget_area.show();
383 }
383 }
384 },
384 },
385
385
386 send: function (content) {
386 send: function (content) {
387 // Send a custom msg associated with this view.
387 // Send a custom msg associated with this view.
388 this.model.send(content, this.callbacks());
388 this.model.send(content, this.callbacks());
389 },
389 },
390
390
391 touch: function () {
391 touch: function () {
392 this.model.save_changes(this.callbacks());
392 this.model.save_changes(this.callbacks());
393 },
393 },
394
394
395 after_displayed: function (callback, context) {
395 after_displayed: function (callback, context) {
396 // Calls the callback right away is the view is already displayed
396 // Calls the callback right away is the view is already displayed
397 // otherwise, register the callback to the 'displayed' event.
397 // otherwise, register the callback to the 'displayed' event.
398 if (this.is_displayed) {
398 if (this.is_displayed) {
399 callback.apply(context);
399 callback.apply(context);
400 } else {
400 } else {
401 this.on('displayed', callback, context);
401 this.on('displayed', callback, context);
402 }
402 }
403 },
403 },
404 });
404 });
405
405
406
406
407 var DOMWidgetView = WidgetView.extend({
407 var DOMWidgetView = WidgetView.extend({
408 initialize: function (parameters) {
408 initialize: function (parameters) {
409 // Public constructor
409 // Public constructor
410 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
410 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
411 this.on('displayed', this.show, this);
411 this.on('displayed', this.show, this);
412 this.after_displayed(function() {
412 this.after_displayed(function() {
413 this.update_visible(this.model, this.model.get("visible"));
413 this.update_visible(this.model, this.model.get("visible"));
414 this.update_css(this.model, this.model.get("_css"));
414 this.update_css(this.model, this.model.get("_css"));
415 }, this);
415 }, this);
416 this.model.on('msg:custom', this.on_msg, this);
416 this.model.on('msg:custom', this.on_msg, this);
417 this.model.on('change:visible', this.update_visible, this);
417 this.model.on('change:visible', this.update_visible, this);
418 this.model.on('change:_css', this.update_css, this);
418 this.model.on('change:_css', this.update_css, this);
419 },
419 },
420
420
421 on_msg: function(msg) {
421 on_msg: function(msg) {
422 // Handle DOM specific msgs.
422 // Handle DOM specific msgs.
423 switch(msg.msg_type) {
423 switch(msg.msg_type) {
424 case 'add_class':
424 case 'add_class':
425 this.add_class(msg.selector, msg.class_list);
425 this.add_class(msg.selector, msg.class_list);
426 break;
426 break;
427 case 'remove_class':
427 case 'remove_class':
428 this.remove_class(msg.selector, msg.class_list);
428 this.remove_class(msg.selector, msg.class_list);
429 break;
429 break;
430 }
430 }
431 },
431 },
432
432
433 add_class: function (selector, class_list) {
433 add_class: function (selector, class_list) {
434 // Add a DOM class to an element.
434 // Add a DOM class to an element.
435 this._get_selector_element(selector).addClass(class_list);
435 this._get_selector_element(selector).addClass(class_list);
436 },
436 },
437
437
438 remove_class: function (selector, class_list) {
438 remove_class: function (selector, class_list) {
439 // Remove a DOM class from an element.
439 // Remove a DOM class from an element.
440 this._get_selector_element(selector).removeClass(class_list);
440 this._get_selector_element(selector).removeClass(class_list);
441 },
441 },
442
442
443 update_visible: function(model, value) {
443 update_visible: function(model, value) {
444 // Update visibility
444 // Update visibility
445 this.$el.toggle(value);
445 this.$el.toggle(value);
446 },
446 },
447
447
448 update_css: function (model, css) {
448 update_css: function (model, css) {
449 // Update the css styling of this view.
449 // Update the css styling of this view.
450 var e = this.$el;
450 var e = this.$el;
451 if (css === undefined) {return;}
451 if (css === undefined) {return;}
452 for (var i = 0; i < css.length; i++) {
452 for (var i = 0; i < css.length; i++) {
453 // Apply the css traits to all elements that match the selector.
453 // Apply the css traits to all elements that match the selector.
454 var selector = css[i][0];
454 var selector = css[i][0];
455 var elements = this._get_selector_element(selector);
455 var elements = this._get_selector_element(selector);
456 if (elements.length > 0) {
456 if (elements.length > 0) {
457 var trait_key = css[i][1];
457 var trait_key = css[i][1];
458 var trait_value = css[i][2];
458 var trait_value = css[i][2];
459 elements.css(trait_key ,trait_value);
459 elements.css(trait_key ,trait_value);
460 }
460 }
461 }
461 }
462 },
462 },
463
463
464 _get_selector_element: function (selector) {
464 _get_selector_element: function (selector) {
465 // Get the elements via the css selector.
465 // Get the elements via the css selector.
466
467 // If the selector is blank, apply the style to the $el_to_style
468 // element. If the $el_to_style element is not defined, use apply
469 // the style to the view's element.
470 var elements;
466 var elements;
471 if (!selector) {
467 if (!selector) {
472 if (this.$el_to_style === undefined) {
468 elements = this.$el;
473 elements = this.$el;
474 } else {
475 elements = this.$el_to_style;
476 }
477 } else {
469 } else {
478 elements = this.$el.find(selector);
470 elements = this.$el.find(selector).addBack(selector);
479 }
471 }
480 return elements;
472 return elements;
481 },
473 },
482 });
474 });
483
475
484 var widget = {
476 var widget = {
485 'WidgetModel': WidgetModel,
477 'WidgetModel': WidgetModel,
486 'WidgetView': WidgetView,
478 'WidgetView': WidgetView,
487 'DOMWidgetView': DOMWidgetView,
479 'DOMWidgetView': DOMWidgetView,
488 };
480 };
489
481
490 // For backwards compatability.
482 // For backwards compatability.
491 $.extend(IPython, widget);
483 $.extend(IPython, widget);
492
484
493 return widget;
485 return widget;
494 });
486 });
@@ -1,120 +1,119 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([
4 define([
5 "widgets/js/widget",
5 "widgets/js/widget",
6 "jquery",
6 "jquery",
7 "bootstrap",
7 "bootstrap",
8 ], function(widget, $){
8 ], function(widget, $){
9
9
10 var CheckboxView = widget.DOMWidgetView.extend({
10 var CheckboxView = widget.DOMWidgetView.extend({
11 render : function(){
11 render : function(){
12 // Called when view is rendered.
12 // Called when view is rendered.
13 this.$el
13 this.$el
14 .addClass('widget-hbox-single');
14 .addClass('widget-hbox-single');
15 this.$label = $('<div />')
15 this.$label = $('<div />')
16 .addClass('widget-hlabel')
16 .addClass('widget-hlabel')
17 .appendTo(this.$el)
17 .appendTo(this.$el)
18 .hide();
18 .hide();
19 this.$checkbox = $('<input />')
19 this.$checkbox = $('<input />')
20 .attr('type', 'checkbox')
20 .attr('type', 'checkbox')
21 .appendTo(this.$el)
21 .appendTo(this.$el)
22 .click($.proxy(this.handle_click, this));
22 .click($.proxy(this.handle_click, this));
23
23
24 this.$el_to_style = this.$checkbox; // Set default element to style
25 this.update(); // Set defaults.
24 this.update(); // Set defaults.
26 },
25 },
27
26
28 handle_click: function() {
27 handle_click: function() {
29 // Handles when the checkbox is clicked.
28 // Handles when the checkbox is clicked.
30
29
31 // Calling model.set will trigger all of the other views of the
30 // Calling model.set will trigger all of the other views of the
32 // model to update.
31 // model to update.
33 var value = this.model.get('value');
32 var value = this.model.get('value');
34 this.model.set('value', ! value, {updated_view: this});
33 this.model.set('value', ! value, {updated_view: this});
35 this.touch();
34 this.touch();
36 },
35 },
37
36
38 update : function(options){
37 update : function(options){
39 // Update the contents of this view
38 // Update the contents of this view
40 //
39 //
41 // Called when the model is changed. The model may have been
40 // Called when the model is changed. The model may have been
42 // changed by another view or by a state update from the back-end.
41 // changed by another view or by a state update from the back-end.
43 this.$checkbox.prop('checked', this.model.get('value'));
42 this.$checkbox.prop('checked', this.model.get('value'));
44
43
45 if (options === undefined || options.updated_view != this) {
44 if (options === undefined || options.updated_view != this) {
46 var disabled = this.model.get('disabled');
45 var disabled = this.model.get('disabled');
47 this.$checkbox.prop('disabled', disabled);
46 this.$checkbox.prop('disabled', disabled);
48
47
49 var description = this.model.get('description');
48 var description = this.model.get('description');
50 if (description.trim().length === 0) {
49 if (description.trim().length === 0) {
51 this.$label.hide();
50 this.$label.hide();
52 } else {
51 } else {
53 this.$label.text(description);
52 this.$label.text(description);
54 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
53 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
55 this.$label.show();
54 this.$label.show();
56 }
55 }
57 }
56 }
58 return CheckboxView.__super__.update.apply(this);
57 return CheckboxView.__super__.update.apply(this);
59 },
58 },
60
59
61 });
60 });
62
61
63
62
64 var ToggleButtonView = widget.DOMWidgetView.extend({
63 var ToggleButtonView = widget.DOMWidgetView.extend({
65 render : function() {
64 render : function() {
66 // Called when view is rendered.
65 // Called when view is rendered.
67 var that = this;
66 var that = this;
68 this.setElement($('<button />')
67 this.setElement($('<button />')
69 .addClass('btn btn-default')
68 .addClass('btn btn-default')
70 .attr('type', 'button')
69 .attr('type', 'button')
71 .on('click', function (e) {
70 .on('click', function (e) {
72 e.preventDefault();
71 e.preventDefault();
73 that.handle_click();
72 that.handle_click();
74 }));
73 }));
75
74
76 this.update(); // Set defaults.
75 this.update(); // Set defaults.
77 },
76 },
78
77
79 update : function(options){
78 update : function(options){
80 // Update the contents of this view
79 // Update the contents of this view
81 //
80 //
82 // Called when the model is changed. The model may have been
81 // Called when the model is changed. The model may have been
83 // changed by another view or by a state update from the back-end.
82 // changed by another view or by a state update from the back-end.
84 if (this.model.get('value')) {
83 if (this.model.get('value')) {
85 this.$el.addClass('active');
84 this.$el.addClass('active');
86 } else {
85 } else {
87 this.$el.removeClass('active');
86 this.$el.removeClass('active');
88 }
87 }
89
88
90 if (options === undefined || options.updated_view != this) {
89 if (options === undefined || options.updated_view != this) {
91
90
92 var disabled = this.model.get('disabled');
91 var disabled = this.model.get('disabled');
93 this.$el.prop('disabled', disabled);
92 this.$el.prop('disabled', disabled);
94
93
95 var description = this.model.get('description');
94 var description = this.model.get('description');
96 if (description.trim().length === 0) {
95 if (description.trim().length === 0) {
97 this.$el.html("&nbsp;"); // Preserve button height
96 this.$el.html("&nbsp;"); // Preserve button height
98 } else {
97 } else {
99 this.$el.text(description);
98 this.$el.text(description);
100 }
99 }
101 }
100 }
102 return ToggleButtonView.__super__.update.apply(this);
101 return ToggleButtonView.__super__.update.apply(this);
103 },
102 },
104
103
105 handle_click: function(e) {
104 handle_click: function(e) {
106 // Handles and validates user input.
105 // Handles and validates user input.
107
106
108 // Calling model.set will trigger all of the other views of the
107 // Calling model.set will trigger all of the other views of the
109 // model to update.
108 // model to update.
110 var value = this.model.get('value');
109 var value = this.model.get('value');
111 this.model.set('value', ! value, {updated_view: this});
110 this.model.set('value', ! value, {updated_view: this});
112 this.touch();
111 this.touch();
113 },
112 },
114 });
113 });
115
114
116 return {
115 return {
117 'CheckboxView': CheckboxView,
116 'CheckboxView': CheckboxView,
118 'ToggleButtonView': ToggleButtonView,
117 'ToggleButtonView': ToggleButtonView,
119 };
118 };
120 });
119 });
@@ -1,284 +1,283 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([
4 define([
5 "widgets/js/widget",
5 "widgets/js/widget",
6 "jqueryui",
6 "jqueryui",
7 "bootstrap",
7 "bootstrap",
8 ], function(widget, $){
8 ], function(widget, $){
9
9
10 var ContainerView = widget.DOMWidgetView.extend({
10 var ContainerView = widget.DOMWidgetView.extend({
11 initialize: function(){
11 initialize: function(){
12 // Public constructor
12 // Public constructor
13 ContainerView.__super__.initialize.apply(this, arguments);
13 ContainerView.__super__.initialize.apply(this, arguments);
14 this.update_children([], this.model.get('children'));
14 this.update_children([], this.model.get('children'));
15 this.model.on('change:children', function(model, value) {
15 this.model.on('change:children', function(model, value) {
16 this.update_children(model.previous('children'), value);
16 this.update_children(model.previous('children'), value);
17 }, this);
17 }, this);
18 },
18 },
19
19
20 render: function(){
20 render: function(){
21 // Called when view is rendered.
21 // Called when view is rendered.
22 this.$el.addClass('widget-container').addClass('vbox');
22 this.$el.addClass('widget-container').addClass('vbox');
23 },
23 },
24
24
25 update_children: function(old_list, new_list) {
25 update_children: function(old_list, new_list) {
26 // Called when the children list changes.
26 // Called when the children list changes.
27 this.do_diff(old_list, new_list,
27 this.do_diff(old_list, new_list,
28 $.proxy(this.remove_child_model, this),
28 $.proxy(this.remove_child_model, this),
29 $.proxy(this.add_child_model, this));
29 $.proxy(this.add_child_model, this));
30 },
30 },
31
31
32 remove_child_model: function(model) {
32 remove_child_model: function(model) {
33 // Called when a model is removed from the children list.
33 // Called when a model is removed from the children list.
34 this.pop_child_view(model).remove();
34 this.pop_child_view(model).remove();
35 },
35 },
36
36
37 add_child_model: function(model) {
37 add_child_model: function(model) {
38 // Called when a model is added to the children list.
38 // Called when a model is added to the children list.
39 var view = this.create_child_view(model);
39 var view = this.create_child_view(model);
40 this.$el.append(view.$el);
40 this.$el.append(view.$el);
41
41
42 // Trigger the displayed event of the child view.
42 // Trigger the displayed event of the child view.
43 this.after_displayed(function() {
43 this.after_displayed(function() {
44 view.trigger('displayed');
44 view.trigger('displayed');
45 });
45 });
46 },
46 },
47 });
47 });
48
48
49
49
50 var PopupView = widget.DOMWidgetView.extend({
50 var PopupView = widget.DOMWidgetView.extend({
51 render: function(){
51 render: function(){
52 // Called when view is rendered.
52 // Called when view is rendered.
53 var that = this;
53 var that = this;
54
54
55 this.$el.on("remove", function(){
55 this.$el.on("remove", function(){
56 that.$backdrop.remove();
56 that.$backdrop.remove();
57 });
57 });
58 this.$backdrop = $('<div />')
58 this.$backdrop = $('<div />')
59 .appendTo($('#notebook-container'))
59 .appendTo($('#notebook-container'))
60 .addClass('modal-dialog')
60 .addClass('modal-dialog')
61 .css('position', 'absolute')
61 .css('position', 'absolute')
62 .css('left', '0px')
62 .css('left', '0px')
63 .css('top', '0px');
63 .css('top', '0px');
64 this.$window = $('<div />')
64 this.$window = $('<div />')
65 .appendTo(this.$backdrop)
65 .appendTo(this.$backdrop)
66 .addClass('modal-content widget-modal')
66 .addClass('modal-content widget-modal')
67 .mousedown(function(){
67 .mousedown(function(){
68 that.bring_to_front();
68 that.bring_to_front();
69 });
69 });
70
70
71 // Set the elements array since the this.$window element is not child
71 // Set the elements array since the this.$window element is not child
72 // of this.$el and the parent widget manager or other widgets may
72 // of this.$el and the parent widget manager or other widgets may
73 // need to know about all of the top-level widgets. The IPython
73 // need to know about all of the top-level widgets. The IPython
74 // widget manager uses this to register the elements with the
74 // widget manager uses this to register the elements with the
75 // keyboard manager.
75 // keyboard manager.
76 this.additional_elements = [this.$window];
76 this.additional_elements = [this.$window];
77
77
78 this.$title_bar = $('<div />')
78 this.$title_bar = $('<div />')
79 .addClass('popover-title')
79 .addClass('popover-title')
80 .appendTo(this.$window)
80 .appendTo(this.$window)
81 .mousedown(function(){
81 .mousedown(function(){
82 that.bring_to_front();
82 that.bring_to_front();
83 });
83 });
84 this.$close = $('<button />')
84 this.$close = $('<button />')
85 .addClass('close fa fa-remove')
85 .addClass('close fa fa-remove')
86 .css('margin-left', '5px')
86 .css('margin-left', '5px')
87 .appendTo(this.$title_bar)
87 .appendTo(this.$title_bar)
88 .click(function(){
88 .click(function(){
89 that.hide();
89 that.hide();
90 event.stopPropagation();
90 event.stopPropagation();
91 });
91 });
92 this.$minimize = $('<button />')
92 this.$minimize = $('<button />')
93 .addClass('close fa fa-arrow-down')
93 .addClass('close fa fa-arrow-down')
94 .appendTo(this.$title_bar)
94 .appendTo(this.$title_bar)
95 .click(function(){
95 .click(function(){
96 that.popped_out = !that.popped_out;
96 that.popped_out = !that.popped_out;
97 if (!that.popped_out) {
97 if (!that.popped_out) {
98 that.$minimize
98 that.$minimize
99 .removeClass('fa fa-arrow-down')
99 .removeClass('fa fa-arrow-down')
100 .addClass('fa fa-arrow-up');
100 .addClass('fa fa-arrow-up');
101
101
102 that.$window
102 that.$window
103 .draggable('destroy')
103 .draggable('destroy')
104 .resizable('destroy')
104 .resizable('destroy')
105 .removeClass('widget-modal modal-content')
105 .removeClass('widget-modal modal-content')
106 .addClass('docked-widget-modal')
106 .addClass('docked-widget-modal')
107 .detach()
107 .detach()
108 .insertBefore(that.$show_button);
108 .insertBefore(that.$show_button);
109 that.$show_button.hide();
109 that.$show_button.hide();
110 that.$close.hide();
110 that.$close.hide();
111 } else {
111 } else {
112 that.$minimize
112 that.$minimize
113 .addClass('fa fa-arrow-down')
113 .addClass('fa fa-arrow-down')
114 .removeClass('fa fa-arrow-up');
114 .removeClass('fa fa-arrow-up');
115
115
116 that.$window
116 that.$window
117 .removeClass('docked-widget-modal')
117 .removeClass('docked-widget-modal')
118 .addClass('widget-modal modal-content')
118 .addClass('widget-modal modal-content')
119 .detach()
119 .detach()
120 .appendTo(that.$backdrop)
120 .appendTo(that.$backdrop)
121 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
121 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
122 .resizable()
122 .resizable()
123 .children('.ui-resizable-handle').show();
123 .children('.ui-resizable-handle').show();
124 that.show();
124 that.show();
125 that.$show_button.show();
125 that.$show_button.show();
126 that.$close.show();
126 that.$close.show();
127 }
127 }
128 event.stopPropagation();
128 event.stopPropagation();
129 });
129 });
130 this.$title = $('<div />')
130 this.$title = $('<div />')
131 .addClass('widget-modal-title')
131 .addClass('widget-modal-title')
132 .html("&nbsp;")
132 .html("&nbsp;")
133 .appendTo(this.$title_bar);
133 .appendTo(this.$title_bar);
134 this.$body = $('<div />')
134 this.$body = $('<div />')
135 .addClass('modal-body')
135 .addClass('modal-body')
136 .addClass('widget-modal-body')
136 .addClass('widget-modal-body')
137 .addClass('widget-container')
137 .addClass('widget-container')
138 .addClass('vbox')
138 .addClass('vbox')
139 .appendTo(this.$window);
139 .appendTo(this.$window);
140
140
141 this.$show_button = $('<button />')
141 this.$show_button = $('<button />')
142 .html("&nbsp;")
142 .html("&nbsp;")
143 .addClass('btn btn-info widget-modal-show')
143 .addClass('btn btn-info widget-modal-show')
144 .appendTo(this.$el)
144 .appendTo(this.$el)
145 .click(function(){
145 .click(function(){
146 that.show();
146 that.show();
147 });
147 });
148
148
149 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
149 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
150 this.$window.resizable();
150 this.$window.resizable();
151 this.$window.on('resize', function(){
151 this.$window.on('resize', function(){
152 that.$body.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
152 that.$body.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
153 });
153 });
154
154
155 this.$el_to_style = this.$body;
156 this._shown_once = false;
155 this._shown_once = false;
157 this.popped_out = true;
156 this.popped_out = true;
158
157
159 this.update_children([], this.model.get('children'));
158 this.update_children([], this.model.get('children'));
160 this.model.on('change:children', function(model, value) {
159 this.model.on('change:children', function(model, value) {
161 this.update_children(model.previous('children'), value);
160 this.update_children(model.previous('children'), value);
162 }, this);
161 }, this);
163 },
162 },
164
163
165 hide: function() {
164 hide: function() {
166 // Called when the modal hide button is clicked.
165 // Called when the modal hide button is clicked.
167 this.$window.hide();
166 this.$window.hide();
168 this.$show_button.removeClass('btn-info');
167 this.$show_button.removeClass('btn-info');
169 },
168 },
170
169
171 show: function() {
170 show: function() {
172 // Called when the modal show button is clicked.
171 // Called when the modal show button is clicked.
173 this.$show_button.addClass('btn-info');
172 this.$show_button.addClass('btn-info');
174 this.$window.show();
173 this.$window.show();
175 if (this.popped_out) {
174 if (this.popped_out) {
176 this.$window.css("positon", "absolute");
175 this.$window.css("positon", "absolute");
177 this.$window.css("top", "0px");
176 this.$window.css("top", "0px");
178 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
177 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
179 $(window).scrollLeft()) + "px");
178 $(window).scrollLeft()) + "px");
180 this.bring_to_front();
179 this.bring_to_front();
181 }
180 }
182 },
181 },
183
182
184 bring_to_front: function() {
183 bring_to_front: function() {
185 // Make the modal top-most, z-ordered about the other modals.
184 // Make the modal top-most, z-ordered about the other modals.
186 var $widget_modals = $(".widget-modal");
185 var $widget_modals = $(".widget-modal");
187 var max_zindex = 0;
186 var max_zindex = 0;
188 $widget_modals.each(function (index, el){
187 $widget_modals.each(function (index, el){
189 var zindex = parseInt($(el).css('z-index'));
188 var zindex = parseInt($(el).css('z-index'));
190 if (!isNaN(zindex)) {
189 if (!isNaN(zindex)) {
191 max_zindex = Math.max(max_zindex, zindex);
190 max_zindex = Math.max(max_zindex, zindex);
192 }
191 }
193 });
192 });
194
193
195 // Start z-index of widget modals at 2000
194 // Start z-index of widget modals at 2000
196 max_zindex = Math.max(max_zindex, 2000);
195 max_zindex = Math.max(max_zindex, 2000);
197
196
198 $widget_modals.each(function (index, el){
197 $widget_modals.each(function (index, el){
199 $el = $(el);
198 $el = $(el);
200 if (max_zindex == parseInt($el.css('z-index'))) {
199 if (max_zindex == parseInt($el.css('z-index'))) {
201 $el.css('z-index', max_zindex - 1);
200 $el.css('z-index', max_zindex - 1);
202 }
201 }
203 });
202 });
204 this.$window.css('z-index', max_zindex);
203 this.$window.css('z-index', max_zindex);
205 },
204 },
206
205
207 update_children: function(old_list, new_list) {
206 update_children: function(old_list, new_list) {
208 // Called when the children list is modified.
207 // Called when the children list is modified.
209 this.do_diff(old_list, new_list,
208 this.do_diff(old_list, new_list,
210 $.proxy(this.remove_child_model, this),
209 $.proxy(this.remove_child_model, this),
211 $.proxy(this.add_child_model, this));
210 $.proxy(this.add_child_model, this));
212 },
211 },
213
212
214 remove_child_model: function(model) {
213 remove_child_model: function(model) {
215 // Called when a child is removed from children list.
214 // Called when a child is removed from children list.
216 this.pop_child_view(model).remove();
215 this.pop_child_view(model).remove();
217 },
216 },
218
217
219 add_child_model: function(model) {
218 add_child_model: function(model) {
220 // Called when a child is added to children list.
219 // Called when a child is added to children list.
221 var view = this.create_child_view(model);
220 var view = this.create_child_view(model);
222 this.$body.append(view.$el);
221 this.$body.append(view.$el);
223
222
224 // Trigger the displayed event of the child view.
223 // Trigger the displayed event of the child view.
225 this.after_displayed(function() {
224 this.after_displayed(function() {
226 view.trigger('displayed');
225 view.trigger('displayed');
227 });
226 });
228 },
227 },
229
228
230 update: function(){
229 update: function(){
231 // Update the contents of this view
230 // Update the contents of this view
232 //
231 //
233 // Called when the model is changed. The model may have been
232 // Called when the model is changed. The model may have been
234 // changed by another view or by a state update from the back-end.
233 // changed by another view or by a state update from the back-end.
235 var description = this.model.get('description');
234 var description = this.model.get('description');
236 if (description.trim().length === 0) {
235 if (description.trim().length === 0) {
237 this.$title.html("&nbsp;"); // Preserve title height
236 this.$title.html("&nbsp;"); // Preserve title height
238 } else {
237 } else {
239 this.$title.text(description);
238 this.$title.text(description);
240 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
239 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
241 }
240 }
242
241
243 var button_text = this.model.get('button_text');
242 var button_text = this.model.get('button_text');
244 if (button_text.trim().length === 0) {
243 if (button_text.trim().length === 0) {
245 this.$show_button.html("&nbsp;"); // Preserve button height
244 this.$show_button.html("&nbsp;"); // Preserve button height
246 } else {
245 } else {
247 this.$show_button.text(button_text);
246 this.$show_button.text(button_text);
248 }
247 }
249
248
250 if (!this._shown_once) {
249 if (!this._shown_once) {
251 this._shown_once = true;
250 this._shown_once = true;
252 this.show();
251 this.show();
253 }
252 }
254
253
255 return PopupView.__super__.update.apply(this);
254 return PopupView.__super__.update.apply(this);
256 },
255 },
257
256
258 _get_selector_element: function(selector) {
257 _get_selector_element: function(selector) {
259 // Get an element view a 'special' jquery selector. (see widget.js)
258 // Get an element view a 'special' jquery selector. (see widget.js)
260 //
259 //
261 // Since the modal actually isn't within the $el in the DOM, we need to extend
260 // Since the modal actually isn't within the $el in the DOM, we need to extend
262 // the selector logic to allow the user to set css on the modal if need be.
261 // the selector logic to allow the user to set css on the modal if need be.
263 // The convention used is:
262 // The convention used is:
264 // "modal" - select the modal div
263 // "modal" - select the modal div
265 // "modal [selector]" - select element(s) within the modal div.
264 // "modal [selector]" - select element(s) within the modal div.
266 // "[selector]" - select elements within $el
265 // "[selector]" - select elements within $el
267 // "" - select the $el_to_style
266 // "" - select the $el
268 if (selector.substring(0, 5) == 'modal') {
267 if (selector.substring(0, 5) == 'modal') {
269 if (selector == 'modal') {
268 if (selector == 'modal') {
270 return this.$window;
269 return this.$window;
271 } else {
270 } else {
272 return this.$window.find(selector.substring(6));
271 return this.$window.find(selector.substring(6));
273 }
272 }
274 } else {
273 } else {
275 return PopupView.__super__._get_selector_element.apply(this, [selector]);
274 return PopupView.__super__._get_selector_element.apply(this, [selector]);
276 }
275 }
277 },
276 },
278 });
277 });
279
278
280 return {
279 return {
281 'ContainerView': ContainerView,
280 'ContainerView': ContainerView,
282 'PopupView': PopupView,
281 'PopupView': PopupView,
283 };
282 };
284 });
283 });
@@ -1,301 +1,298 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([
4 define([
5 "widgets/js/widget",
5 "widgets/js/widget",
6 "jqueryui",
6 "jqueryui",
7 "bootstrap",
7 "bootstrap",
8 ], function(widget, $){
8 ], function(widget, $){
9
9
10 var IntSliderView = widget.DOMWidgetView.extend({
10 var IntSliderView = widget.DOMWidgetView.extend({
11 render : function(){
11 render : function(){
12 // Called when view is rendered.
12 // Called when view is rendered.
13 this.$el
13 this.$el
14 .addClass('widget-hbox-single');
14 .addClass('widget-hbox-single');
15 this.$label = $('<div />')
15 this.$label = $('<div />')
16 .appendTo(this.$el)
16 .appendTo(this.$el)
17 .addClass('widget-hlabel')
17 .addClass('widget-hlabel')
18 .hide();
18 .hide();
19
19
20 this.$slider = $('<div />')
20 this.$slider = $('<div />')
21 .slider({})
21 .slider({})
22 .addClass('slider');
22 .addClass('slider');
23 // Put the slider in a container
23 // Put the slider in a container
24 this.$slider_container = $('<div />')
24 this.$slider_container = $('<div />')
25 .addClass('widget-hslider')
25 .addClass('widget-hslider')
26 .append(this.$slider);
26 .append(this.$slider);
27 this.$el_to_style = this.$slider_container; // Set default element to style
28 this.$el.append(this.$slider_container);
27 this.$el.append(this.$slider_container);
29
28
30 this.$readout = $('<div/>')
29 this.$readout = $('<div/>')
31 .appendTo(this.$el)
30 .appendTo(this.$el)
32 .addClass('widget-hreadout')
31 .addClass('widget-hreadout')
33 .hide();
32 .hide();
34
33
35 // Set defaults.
34 // Set defaults.
36 this.update();
35 this.update();
37 },
36 },
38
37
39 update : function(options){
38 update : function(options){
40 // Update the contents of this view
39 // Update the contents of this view
41 //
40 //
42 // Called when the model is changed. The model may have been
41 // Called when the model is changed. The model may have been
43 // changed by another view or by a state update from the back-end.
42 // changed by another view or by a state update from the back-end.
44 if (options === undefined || options.updated_view != this) {
43 if (options === undefined || options.updated_view != this) {
45 // JQuery slider option keys. These keys happen to have a
44 // JQuery slider option keys. These keys happen to have a
46 // one-to-one mapping with the corrosponding keys of the model.
45 // one-to-one mapping with the corrosponding keys of the model.
47 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
46 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
48 var that = this;
47 var that = this;
49 that.$slider.slider({});
48 that.$slider.slider({});
50 _.each(jquery_slider_keys, function(key, i) {
49 _.each(jquery_slider_keys, function(key, i) {
51 var model_value = that.model.get(key);
50 var model_value = that.model.get(key);
52 if (model_value !== undefined) {
51 if (model_value !== undefined) {
53 that.$slider.slider("option", key, model_value);
52 that.$slider.slider("option", key, model_value);
54 }
53 }
55 });
54 });
56
55
57 // WORKAROUND FOR JQUERY SLIDER BUG.
56 // WORKAROUND FOR JQUERY SLIDER BUG.
58 // The horizontal position of the slider handle
57 // The horizontal position of the slider handle
59 // depends on the value of the slider at the time
58 // depends on the value of the slider at the time
60 // of orientation change. Before applying the new
59 // of orientation change. Before applying the new
61 // workaround, we set the value to the minimum to
60 // workaround, we set the value to the minimum to
62 // make sure that the horizontal placement of the
61 // make sure that the horizontal placement of the
63 // handle in the vertical slider is always
62 // handle in the vertical slider is always
64 // consistent.
63 // consistent.
65 var orientation = this.model.get('orientation');
64 var orientation = this.model.get('orientation');
66 var value = this.model.get('min');
65 var value = this.model.get('min');
67 this.$slider.slider('option', 'value', value);
66 this.$slider.slider('option', 'value', value);
68 this.$slider.slider('option', 'orientation', orientation);
67 this.$slider.slider('option', 'orientation', orientation);
69 value = this.model.get('value');
68 value = this.model.get('value');
70 this.$slider.slider('option', 'value', value);
69 this.$slider.slider('option', 'value', value);
71 this.$readout.text(value);
70 this.$readout.text(value);
72
71
73 // Use the right CSS classes for vertical & horizontal sliders
72 // Use the right CSS classes for vertical & horizontal sliders
74 if (orientation=='vertical') {
73 if (orientation=='vertical') {
75 this.$slider_container
74 this.$slider_container
76 .removeClass('widget-hslider')
75 .removeClass('widget-hslider')
77 .addClass('widget-vslider');
76 .addClass('widget-vslider');
78 this.$el
77 this.$el
79 .removeClass('widget-hbox-single')
78 .removeClass('widget-hbox-single')
80 .addClass('widget-vbox-single');
79 .addClass('widget-vbox-single');
81 this.$label
80 this.$label
82 .removeClass('widget-hlabel')
81 .removeClass('widget-hlabel')
83 .addClass('widget-vlabel');
82 .addClass('widget-vlabel');
84 this.$readout
83 this.$readout
85 .removeClass('widget-hreadout')
84 .removeClass('widget-hreadout')
86 .addClass('widget-vreadout');
85 .addClass('widget-vreadout');
87
86
88 } else {
87 } else {
89 this.$slider_container
88 this.$slider_container
90 .removeClass('widget-vslider')
89 .removeClass('widget-vslider')
91 .addClass('widget-hslider');
90 .addClass('widget-hslider');
92 this.$el
91 this.$el
93 .removeClass('widget-vbox-single')
92 .removeClass('widget-vbox-single')
94 .addClass('widget-hbox-single');
93 .addClass('widget-hbox-single');
95 this.$label
94 this.$label
96 .removeClass('widget-vlabel')
95 .removeClass('widget-vlabel')
97 .addClass('widget-hlabel');
96 .addClass('widget-hlabel');
98 this.$readout
97 this.$readout
99 .removeClass('widget-vreadout')
98 .removeClass('widget-vreadout')
100 .addClass('widget-hreadout');
99 .addClass('widget-hreadout');
101 }
100 }
102
101
103 var description = this.model.get('description');
102 var description = this.model.get('description');
104 if (description.length === 0) {
103 if (description.length === 0) {
105 this.$label.hide();
104 this.$label.hide();
106 } else {
105 } else {
107 this.$label.text(description);
106 this.$label.text(description);
108 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
107 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
109 this.$label.show();
108 this.$label.show();
110 }
109 }
111
110
112 var readout = this.model.get('readout');
111 var readout = this.model.get('readout');
113 if (readout) {
112 if (readout) {
114 this.$readout.show();
113 this.$readout.show();
115 } else {
114 } else {
116 this.$readout.hide();
115 this.$readout.hide();
117 }
116 }
118 }
117 }
119 return IntSliderView.__super__.update.apply(this);
118 return IntSliderView.__super__.update.apply(this);
120 },
119 },
121
120
122 events: {
121 events: {
123 // Dictionary of events and their handlers.
122 // Dictionary of events and their handlers.
124 "slide" : "handleSliderChange"
123 "slide" : "handleSliderChange"
125 },
124 },
126
125
127 handleSliderChange: function(e, ui) {
126 handleSliderChange: function(e, ui) {
128 // Called when the slider value is changed.
127 // Called when the slider value is changed.
129
128
130 // Calling model.set will trigger all of the other views of the
129 // Calling model.set will trigger all of the other views of the
131 // model to update.
130 // model to update.
132 var actual_value = this._validate_slide_value(ui.value);
131 var actual_value = this._validate_slide_value(ui.value);
133 this.model.set('value', actual_value, {updated_view: this});
132 this.model.set('value', actual_value, {updated_view: this});
134 this.$readout.text(actual_value);
133 this.$readout.text(actual_value);
135 this.touch();
134 this.touch();
136 },
135 },
137
136
138 _validate_slide_value: function(x) {
137 _validate_slide_value: function(x) {
139 // Validate the value of the slider before sending it to the back-end
138 // Validate the value of the slider before sending it to the back-end
140 // and applying it to the other views on the page.
139 // and applying it to the other views on the page.
141
140
142 // Double bit-wise not truncates the decimel (int cast).
141 // Double bit-wise not truncates the decimel (int cast).
143 return ~~x;
142 return ~~x;
144 },
143 },
145 });
144 });
146
145
147
146
148 var IntTextView = widget.DOMWidgetView.extend({
147 var IntTextView = widget.DOMWidgetView.extend({
149 render : function(){
148 render : function(){
150 // Called when view is rendered.
149 // Called when view is rendered.
151 this.$el
150 this.$el
152 .addClass('widget-hbox-single');
151 .addClass('widget-hbox-single');
153 this.$label = $('<div />')
152 this.$label = $('<div />')
154 .appendTo(this.$el)
153 .appendTo(this.$el)
155 .addClass('widget-hlabel')
154 .addClass('widget-hlabel')
156 .hide();
155 .hide();
157 this.$textbox = $('<input type="text" />')
156 this.$textbox = $('<input type="text" />')
158 .addClass('form-control')
157 .addClass('form-control')
159 .addClass('widget-numeric-text')
158 .addClass('widget-numeric-text')
160 .appendTo(this.$el);
159 .appendTo(this.$el);
161 this.$el_to_style = this.$textbox; // Set default element to style
162 this.update(); // Set defaults.
160 this.update(); // Set defaults.
163 },
161 },
164
162
165 update : function(options){
163 update : function(options){
166 // Update the contents of this view
164 // Update the contents of this view
167 //
165 //
168 // Called when the model is changed. The model may have been
166 // Called when the model is changed. The model may have been
169 // changed by another view or by a state update from the back-end.
167 // changed by another view or by a state update from the back-end.
170 if (options === undefined || options.updated_view != this) {
168 if (options === undefined || options.updated_view != this) {
171 var value = this.model.get('value');
169 var value = this.model.get('value');
172 if (this._parse_value(this.$textbox.val()) != value) {
170 if (this._parse_value(this.$textbox.val()) != value) {
173 this.$textbox.val(value);
171 this.$textbox.val(value);
174 }
172 }
175
173
176 if (this.model.get('disabled')) {
174 if (this.model.get('disabled')) {
177 this.$textbox.attr('disabled','disabled');
175 this.$textbox.attr('disabled','disabled');
178 } else {
176 } else {
179 this.$textbox.removeAttr('disabled');
177 this.$textbox.removeAttr('disabled');
180 }
178 }
181
179
182 var description = this.model.get('description');
180 var description = this.model.get('description');
183 if (description.length === 0) {
181 if (description.length === 0) {
184 this.$label.hide();
182 this.$label.hide();
185 } else {
183 } else {
186 this.$label.text(description);
184 this.$label.text(description);
187 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
185 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
188 this.$label.show();
186 this.$label.show();
189 }
187 }
190 }
188 }
191 return IntTextView.__super__.update.apply(this);
189 return IntTextView.__super__.update.apply(this);
192 },
190 },
193
191
194 events: {
192 events: {
195 // Dictionary of events and their handlers.
193 // Dictionary of events and their handlers.
196 "keyup input" : "handleChanging",
194 "keyup input" : "handleChanging",
197 "paste input" : "handleChanging",
195 "paste input" : "handleChanging",
198 "cut input" : "handleChanging",
196 "cut input" : "handleChanging",
199
197
200 // Fires only when control is validated or looses focus.
198 // Fires only when control is validated or looses focus.
201 "change input" : "handleChanged"
199 "change input" : "handleChanged"
202 },
200 },
203
201
204 handleChanging: function(e) {
202 handleChanging: function(e) {
205 // Handles and validates user input.
203 // Handles and validates user input.
206
204
207 // Try to parse value as a int.
205 // Try to parse value as a int.
208 var numericalValue = 0;
206 var numericalValue = 0;
209 if (e.target.value !== '') {
207 if (e.target.value !== '') {
210 var trimmed = e.target.value.trim();
208 var trimmed = e.target.value.trim();
211 if (!(['-', '-.', '.', '+.', '+'].indexOf(trimmed) >= 0)) {
209 if (!(['-', '-.', '.', '+.', '+'].indexOf(trimmed) >= 0)) {
212 numericalValue = this._parse_value(e.target.value);
210 numericalValue = this._parse_value(e.target.value);
213 }
211 }
214 }
212 }
215
213
216 // If parse failed, reset value to value stored in model.
214 // If parse failed, reset value to value stored in model.
217 if (isNaN(numericalValue)) {
215 if (isNaN(numericalValue)) {
218 e.target.value = this.model.get('value');
216 e.target.value = this.model.get('value');
219 } else if (!isNaN(numericalValue)) {
217 } else if (!isNaN(numericalValue)) {
220 if (this.model.get('max') !== undefined) {
218 if (this.model.get('max') !== undefined) {
221 numericalValue = Math.min(this.model.get('max'), numericalValue);
219 numericalValue = Math.min(this.model.get('max'), numericalValue);
222 }
220 }
223 if (this.model.get('min') !== undefined) {
221 if (this.model.get('min') !== undefined) {
224 numericalValue = Math.max(this.model.get('min'), numericalValue);
222 numericalValue = Math.max(this.model.get('min'), numericalValue);
225 }
223 }
226
224
227 // Apply the value if it has changed.
225 // Apply the value if it has changed.
228 if (numericalValue != this.model.get('value')) {
226 if (numericalValue != this.model.get('value')) {
229
227
230 // Calling model.set will trigger all of the other views of the
228 // Calling model.set will trigger all of the other views of the
231 // model to update.
229 // model to update.
232 this.model.set('value', numericalValue, {updated_view: this});
230 this.model.set('value', numericalValue, {updated_view: this});
233 this.touch();
231 this.touch();
234 }
232 }
235 }
233 }
236 },
234 },
237
235
238 handleChanged: function(e) {
236 handleChanged: function(e) {
239 // Applies validated input.
237 // Applies validated input.
240 if (this.model.get('value') != e.target.value) {
238 if (this.model.get('value') != e.target.value) {
241 e.target.value = this.model.get('value');
239 e.target.value = this.model.get('value');
242 }
240 }
243 },
241 },
244
242
245 _parse_value: function(value) {
243 _parse_value: function(value) {
246 // Parse the value stored in a string.
244 // Parse the value stored in a string.
247 return parseInt(value);
245 return parseInt(value);
248 },
246 },
249 });
247 });
250
248
251
249
252 var ProgressView = widget.DOMWidgetView.extend({
250 var ProgressView = widget.DOMWidgetView.extend({
253 render : function(){
251 render : function(){
254 // Called when view is rendered.
252 // Called when view is rendered.
255 this.$el
253 this.$el
256 .addClass('widget-hbox-single');
254 .addClass('widget-hbox-single');
257 this.$label = $('<div />')
255 this.$label = $('<div />')
258 .appendTo(this.$el)
256 .appendTo(this.$el)
259 .addClass('widget-hlabel')
257 .addClass('widget-hlabel')
260 .hide();
258 .hide();
261 this.$progress = $('<div />')
259 this.$progress = $('<div />')
262 .addClass('progress')
260 .addClass('progress')
263 .addClass('widget-progress')
261 .addClass('widget-progress')
264 .appendTo(this.$el);
262 .appendTo(this.$el);
265 this.$el_to_style = this.$progress; // Set default element to style
266 this.$bar = $('<div />')
263 this.$bar = $('<div />')
267 .addClass('progress-bar')
264 .addClass('progress-bar')
268 .css('width', '50%')
265 .css('width', '50%')
269 .appendTo(this.$progress);
266 .appendTo(this.$progress);
270 this.update(); // Set defaults.
267 this.update(); // Set defaults.
271 },
268 },
272
269
273 update : function(){
270 update : function(){
274 // Update the contents of this view
271 // Update the contents of this view
275 //
272 //
276 // Called when the model is changed. The model may have been
273 // Called when the model is changed. The model may have been
277 // changed by another view or by a state update from the back-end.
274 // changed by another view or by a state update from the back-end.
278 var value = this.model.get('value');
275 var value = this.model.get('value');
279 var max = this.model.get('max');
276 var max = this.model.get('max');
280 var min = this.model.get('min');
277 var min = this.model.get('min');
281 var percent = 100.0 * (value - min) / (max - min);
278 var percent = 100.0 * (value - min) / (max - min);
282 this.$bar.css('width', percent + '%');
279 this.$bar.css('width', percent + '%');
283
280
284 var description = this.model.get('description');
281 var description = this.model.get('description');
285 if (description.length === 0) {
282 if (description.length === 0) {
286 this.$label.hide();
283 this.$label.hide();
287 } else {
284 } else {
288 this.$label.text(description);
285 this.$label.text(description);
289 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
286 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
290 this.$label.show();
287 this.$label.show();
291 }
288 }
292 return ProgressView.__super__.update.apply(this);
289 return ProgressView.__super__.update.apply(this);
293 },
290 },
294 });
291 });
295
292
296 return {
293 return {
297 'IntSliderView': IntSliderView,
294 'IntSliderView': IntSliderView,
298 'IntTextView': IntTextView,
295 'IntTextView': IntTextView,
299 'ProgressView': ProgressView,
296 'ProgressView': ProgressView,
300 };
297 };
301 });
298 });
@@ -1,380 +1,376 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([
4 define([
5 "widgets/js/widget",
5 "widgets/js/widget",
6 "base/js/utils",
6 "base/js/utils",
7 "jquery",
7 "jquery",
8 "bootstrap",
8 "bootstrap",
9 ], function(widget, utils, $){
9 ], function(widget, utils, $){
10
10
11 var DropdownView = widget.DOMWidgetView.extend({
11 var DropdownView = widget.DOMWidgetView.extend({
12 render : function(){
12 render : function(){
13 // Called when view is rendered.
13 // Called when view is rendered.
14 this.$el
14 this.$el
15 .addClass('widget-hbox-single');
15 .addClass('widget-hbox-single');
16 this.$label = $('<div />')
16 this.$label = $('<div />')
17 .appendTo(this.$el)
17 .appendTo(this.$el)
18 .addClass('widget-hlabel')
18 .addClass('widget-hlabel')
19 .hide();
19 .hide();
20 this.$buttongroup = $('<div />')
20 this.$buttongroup = $('<div />')
21 .addClass('widget_item')
21 .addClass('widget_item')
22 .addClass('btn-group')
22 .addClass('btn-group')
23 .appendTo(this.$el);
23 .appendTo(this.$el);
24 this.$el_to_style = this.$buttongroup; // Set default element to style
25 this.$droplabel = $('<button />')
24 this.$droplabel = $('<button />')
26 .addClass('btn btn-default')
25 .addClass('btn btn-default')
27 .addClass('widget-combo-btn')
26 .addClass('widget-combo-btn')
28 .html("&nbsp;")
27 .html("&nbsp;")
29 .appendTo(this.$buttongroup);
28 .appendTo(this.$buttongroup);
30 this.$dropbutton = $('<button />')
29 this.$dropbutton = $('<button />')
31 .addClass('btn btn-default')
30 .addClass('btn btn-default')
32 .addClass('dropdown-toggle')
31 .addClass('dropdown-toggle')
33 .addClass('widget-combo-carrot-btn')
32 .addClass('widget-combo-carrot-btn')
34 .attr('data-toggle', 'dropdown')
33 .attr('data-toggle', 'dropdown')
35 .append($('<span />').addClass("caret"))
34 .append($('<span />').addClass("caret"))
36 .appendTo(this.$buttongroup);
35 .appendTo(this.$buttongroup);
37 this.$droplist = $('<ul />')
36 this.$droplist = $('<ul />')
38 .addClass('dropdown-menu')
37 .addClass('dropdown-menu')
39 .appendTo(this.$buttongroup);
38 .appendTo(this.$buttongroup);
40
39
41 // Set defaults.
40 // Set defaults.
42 this.update();
41 this.update();
43 },
42 },
44
43
45 update : function(options){
44 update : function(options){
46 // Update the contents of this view
45 // Update the contents of this view
47 //
46 //
48 // Called when the model is changed. The model may have been
47 // Called when the model is changed. The model may have been
49 // changed by another view or by a state update from the back-end.
48 // changed by another view or by a state update from the back-end.
50
49
51 if (options === undefined || options.updated_view != this) {
50 if (options === undefined || options.updated_view != this) {
52 var selected_item_text = this.model.get('value_name');
51 var selected_item_text = this.model.get('value_name');
53 if (selected_item_text.trim().length === 0) {
52 if (selected_item_text.trim().length === 0) {
54 this.$droplabel.html("&nbsp;");
53 this.$droplabel.html("&nbsp;");
55 } else {
54 } else {
56 this.$droplabel.text(selected_item_text);
55 this.$droplabel.text(selected_item_text);
57 }
56 }
58
57
59 var items = this.model.get('value_names');
58 var items = this.model.get('value_names');
60 var $replace_droplist = $('<ul />')
59 var $replace_droplist = $('<ul />')
61 .addClass('dropdown-menu');
60 .addClass('dropdown-menu');
62 var that = this;
61 var that = this;
63 _.each(items, function(item, i) {
62 _.each(items, function(item, i) {
64 var item_button = $('<a href="#"/>')
63 var item_button = $('<a href="#"/>')
65 .text(item)
64 .text(item)
66 .on('click', $.proxy(that.handle_click, that));
65 .on('click', $.proxy(that.handle_click, that));
67 $replace_droplist.append($('<li />').append(item_button));
66 $replace_droplist.append($('<li />').append(item_button));
68 });
67 });
69
68
70 this.$droplist.replaceWith($replace_droplist);
69 this.$droplist.replaceWith($replace_droplist);
71 this.$droplist.remove();
70 this.$droplist.remove();
72 this.$droplist = $replace_droplist;
71 this.$droplist = $replace_droplist;
73
72
74 if (this.model.get('disabled')) {
73 if (this.model.get('disabled')) {
75 this.$buttongroup.attr('disabled','disabled');
74 this.$buttongroup.attr('disabled','disabled');
76 this.$droplabel.attr('disabled','disabled');
75 this.$droplabel.attr('disabled','disabled');
77 this.$dropbutton.attr('disabled','disabled');
76 this.$dropbutton.attr('disabled','disabled');
78 this.$droplist.attr('disabled','disabled');
77 this.$droplist.attr('disabled','disabled');
79 } else {
78 } else {
80 this.$buttongroup.removeAttr('disabled');
79 this.$buttongroup.removeAttr('disabled');
81 this.$droplabel.removeAttr('disabled');
80 this.$droplabel.removeAttr('disabled');
82 this.$dropbutton.removeAttr('disabled');
81 this.$dropbutton.removeAttr('disabled');
83 this.$droplist.removeAttr('disabled');
82 this.$droplist.removeAttr('disabled');
84 }
83 }
85
84
86 var description = this.model.get('description');
85 var description = this.model.get('description');
87 if (description.length === 0) {
86 if (description.length === 0) {
88 this.$label.hide();
87 this.$label.hide();
89 } else {
88 } else {
90 this.$label.text(description);
89 this.$label.text(description);
91 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
90 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
92 this.$label.show();
91 this.$label.show();
93 }
92 }
94 }
93 }
95 return DropdownView.__super__.update.apply(this);
94 return DropdownView.__super__.update.apply(this);
96 },
95 },
97
96
98 handle_click: function (e) {
97 handle_click: function (e) {
99 // Handle when a value is clicked.
98 // Handle when a value is clicked.
100
99
101 // Calling model.set will trigger all of the other views of the
100 // Calling model.set will trigger all of the other views of the
102 // model to update.
101 // model to update.
103 this.model.set('value_name', $(e.target).text(), {updated_view: this});
102 this.model.set('value_name', $(e.target).text(), {updated_view: this});
104 this.touch();
103 this.touch();
105 },
104 },
106
105
107 });
106 });
108
107
109
108
110 var RadioButtonsView = widget.DOMWidgetView.extend({
109 var RadioButtonsView = widget.DOMWidgetView.extend({
111 render : function(){
110 render : function(){
112 // Called when view is rendered.
111 // Called when view is rendered.
113 this.$el
112 this.$el
114 .addClass('widget-hbox');
113 .addClass('widget-hbox');
115 this.$label = $('<div />')
114 this.$label = $('<div />')
116 .appendTo(this.$el)
115 .appendTo(this.$el)
117 .addClass('widget-hlabel')
116 .addClass('widget-hlabel')
118 .hide();
117 .hide();
119 this.$container = $('<div />')
118 this.$container = $('<div />')
120 .appendTo(this.$el)
119 .appendTo(this.$el)
121 .addClass('widget-radio-box');
120 .addClass('widget-radio-box');
122 this.$el_to_style = this.$container; // Set default element to style
123 this.update();
121 this.update();
124 },
122 },
125
123
126 update : function(options){
124 update : function(options){
127 // Update the contents of this view
125 // Update the contents of this view
128 //
126 //
129 // Called when the model is changed. The model may have been
127 // Called when the model is changed. The model may have been
130 // changed by another view or by a state update from the back-end.
128 // changed by another view or by a state update from the back-end.
131 if (options === undefined || options.updated_view != this) {
129 if (options === undefined || options.updated_view != this) {
132 // Add missing items to the DOM.
130 // Add missing items to the DOM.
133 var items = this.model.get('value_names');
131 var items = this.model.get('value_names');
134 var disabled = this.model.get('disabled');
132 var disabled = this.model.get('disabled');
135 var that = this;
133 var that = this;
136 _.each(items, function(item, index) {
134 _.each(items, function(item, index) {
137 var item_query = ' :input[value="' + item + '"]';
135 var item_query = ' :input[value="' + item + '"]';
138 if (that.$el.find(item_query).length === 0) {
136 if (that.$el.find(item_query).length === 0) {
139 var $label = $('<label />')
137 var $label = $('<label />')
140 .addClass('radio')
138 .addClass('radio')
141 .text(item)
139 .text(item)
142 .appendTo(that.$container);
140 .appendTo(that.$container);
143
141
144 $('<input />')
142 $('<input />')
145 .attr('type', 'radio')
143 .attr('type', 'radio')
146 .addClass(that.model)
144 .addClass(that.model)
147 .val(item)
145 .val(item)
148 .prependTo($label)
146 .prependTo($label)
149 .on('click', $.proxy(that.handle_click, that));
147 .on('click', $.proxy(that.handle_click, that));
150 }
148 }
151
149
152 var $item_element = that.$container.find(item_query);
150 var $item_element = that.$container.find(item_query);
153 if (that.model.get('value_name') == item) {
151 if (that.model.get('value_name') == item) {
154 $item_element.prop('checked', true);
152 $item_element.prop('checked', true);
155 } else {
153 } else {
156 $item_element.prop('checked', false);
154 $item_element.prop('checked', false);
157 }
155 }
158 $item_element.prop('disabled', disabled);
156 $item_element.prop('disabled', disabled);
159 });
157 });
160
158
161 // Remove items that no longer exist.
159 // Remove items that no longer exist.
162 this.$container.find('input').each(function(i, obj) {
160 this.$container.find('input').each(function(i, obj) {
163 var value = $(obj).val();
161 var value = $(obj).val();
164 var found = false;
162 var found = false;
165 _.each(items, function(item, index) {
163 _.each(items, function(item, index) {
166 if (item == value) {
164 if (item == value) {
167 found = true;
165 found = true;
168 return false;
166 return false;
169 }
167 }
170 });
168 });
171
169
172 if (!found) {
170 if (!found) {
173 $(obj).parent().remove();
171 $(obj).parent().remove();
174 }
172 }
175 });
173 });
176
174
177 var description = this.model.get('description');
175 var description = this.model.get('description');
178 if (description.length === 0) {
176 if (description.length === 0) {
179 this.$label.hide();
177 this.$label.hide();
180 } else {
178 } else {
181 this.$label.text(description);
179 this.$label.text(description);
182 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
180 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
183 this.$label.show();
181 this.$label.show();
184 }
182 }
185 }
183 }
186 return RadioButtonsView.__super__.update.apply(this);
184 return RadioButtonsView.__super__.update.apply(this);
187 },
185 },
188
186
189 handle_click: function (e) {
187 handle_click: function (e) {
190 // Handle when a value is clicked.
188 // Handle when a value is clicked.
191
189
192 // Calling model.set will trigger all of the other views of the
190 // Calling model.set will trigger all of the other views of the
193 // model to update.
191 // model to update.
194 this.model.set('value_name', $(e.target).val(), {updated_view: this});
192 this.model.set('value_name', $(e.target).val(), {updated_view: this});
195 this.touch();
193 this.touch();
196 },
194 },
197 });
195 });
198
196
199
197
200 var ToggleButtonsView = widget.DOMWidgetView.extend({
198 var ToggleButtonsView = widget.DOMWidgetView.extend({
201 render : function(){
199 render : function(){
202 // Called when view is rendered.
200 // Called when view is rendered.
203 this.$el
201 this.$el
204 .addClass('widget-hbox-single');
202 .addClass('widget-hbox-single');
205 this.$label = $('<div />')
203 this.$label = $('<div />')
206 .appendTo(this.$el)
204 .appendTo(this.$el)
207 .addClass('widget-hlabel')
205 .addClass('widget-hlabel')
208 .hide();
206 .hide();
209 this.$buttongroup = $('<div />')
207 this.$buttongroup = $('<div />')
210 .addClass('btn-group')
208 .addClass('btn-group')
211 .attr('data-toggle', 'buttons-radio')
209 .attr('data-toggle', 'buttons-radio')
212 .appendTo(this.$el);
210 .appendTo(this.$el);
213 this.$el_to_style = this.$buttongroup; // Set default element to style
214 this.update();
211 this.update();
215 },
212 },
216
213
217 update : function(options){
214 update : function(options){
218 // Update the contents of this view
215 // Update the contents of this view
219 //
216 //
220 // Called when the model is changed. The model may have been
217 // Called when the model is changed. The model may have been
221 // changed by another view or by a state update from the back-end.
218 // changed by another view or by a state update from the back-end.
222 if (options === undefined || options.updated_view != this) {
219 if (options === undefined || options.updated_view != this) {
223 // Add missing items to the DOM.
220 // Add missing items to the DOM.
224 var items = this.model.get('value_names');
221 var items = this.model.get('value_names');
225 var disabled = this.model.get('disabled');
222 var disabled = this.model.get('disabled');
226 var that = this;
223 var that = this;
227 var item_html;
224 var item_html;
228 _.each(items, function(item, index) {
225 _.each(items, function(item, index) {
229 if (item.trim().length == 0) {
226 if (item.trim().length == 0) {
230 item_html = "&nbsp;";
227 item_html = "&nbsp;";
231 } else {
228 } else {
232 item_html = utils.escape_html(item);
229 item_html = utils.escape_html(item);
233 }
230 }
234 var item_query = '[data-value="' + item + '"]';
231 var item_query = '[data-value="' + item + '"]';
235 var $item_element = that.$buttongroup.find(item_query);
232 var $item_element = that.$buttongroup.find(item_query);
236 if (!$item_element.length) {
233 if (!$item_element.length) {
237 $item_element = $('<button/>')
234 $item_element = $('<button/>')
238 .attr('type', 'button')
235 .attr('type', 'button')
239 .addClass('btn btn-default')
236 .addClass('btn btn-default')
240 .html(item_html)
237 .html(item_html)
241 .appendTo(that.$buttongroup)
238 .appendTo(that.$buttongroup)
242 .attr('data-value', item)
239 .attr('data-value', item)
243 .on('click', $.proxy(that.handle_click, that));
240 .on('click', $.proxy(that.handle_click, that));
244 }
241 }
245 if (that.model.get('value_name') == item) {
242 if (that.model.get('value_name') == item) {
246 $item_element.addClass('active');
243 $item_element.addClass('active');
247 } else {
244 } else {
248 $item_element.removeClass('active');
245 $item_element.removeClass('active');
249 }
246 }
250 $item_element.prop('disabled', disabled);
247 $item_element.prop('disabled', disabled);
251 });
248 });
252
249
253 // Remove items that no longer exist.
250 // Remove items that no longer exist.
254 this.$buttongroup.find('button').each(function(i, obj) {
251 this.$buttongroup.find('button').each(function(i, obj) {
255 var value = $(obj).data('value');
252 var value = $(obj).data('value');
256 var found = false;
253 var found = false;
257 _.each(items, function(item, index) {
254 _.each(items, function(item, index) {
258 if (item == value) {
255 if (item == value) {
259 found = true;
256 found = true;
260 return false;
257 return false;
261 }
258 }
262 });
259 });
263
260
264 if (!found) {
261 if (!found) {
265 $(obj).remove();
262 $(obj).remove();
266 }
263 }
267 });
264 });
268
265
269 var description = this.model.get('description');
266 var description = this.model.get('description');
270 if (description.length === 0) {
267 if (description.length === 0) {
271 this.$label.hide();
268 this.$label.hide();
272 } else {
269 } else {
273 this.$label.text(description);
270 this.$label.text(description);
274 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
271 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
275 this.$label.show();
272 this.$label.show();
276 }
273 }
277 }
274 }
278 return ToggleButtonsView.__super__.update.apply(this);
275 return ToggleButtonsView.__super__.update.apply(this);
279 },
276 },
280
277
281 handle_click: function (e) {
278 handle_click: function (e) {
282 // Handle when a value is clicked.
279 // Handle when a value is clicked.
283
280
284 // Calling model.set will trigger all of the other views of the
281 // Calling model.set will trigger all of the other views of the
285 // model to update.
282 // model to update.
286 this.model.set('value_name', $(e.target).data('value'), {updated_view: this});
283 this.model.set('value_name', $(e.target).data('value'), {updated_view: this});
287 this.touch();
284 this.touch();
288 },
285 },
289 });
286 });
290
287
291
288
292 var SelectView = widget.DOMWidgetView.extend({
289 var SelectView = widget.DOMWidgetView.extend({
293 render : function(){
290 render : function(){
294 // Called when view is rendered.
291 // Called when view is rendered.
295 this.$el
292 this.$el
296 .addClass('widget-hbox');
293 .addClass('widget-hbox');
297 this.$label = $('<div />')
294 this.$label = $('<div />')
298 .appendTo(this.$el)
295 .appendTo(this.$el)
299 .addClass('widget-hlabel')
296 .addClass('widget-hlabel')
300 .hide();
297 .hide();
301 this.$listbox = $('<select />')
298 this.$listbox = $('<select />')
302 .addClass('widget-listbox form-control')
299 .addClass('widget-listbox form-control')
303 .attr('size', 6)
300 .attr('size', 6)
304 .appendTo(this.$el);
301 .appendTo(this.$el);
305 this.$el_to_style = this.$listbox; // Set default element to style
306 this.update();
302 this.update();
307 },
303 },
308
304
309 update : function(options){
305 update : function(options){
310 // Update the contents of this view
306 // Update the contents of this view
311 //
307 //
312 // Called when the model is changed. The model may have been
308 // Called when the model is changed. The model may have been
313 // changed by another view or by a state update from the back-end.
309 // changed by another view or by a state update from the back-end.
314 if (options === undefined || options.updated_view != this) {
310 if (options === undefined || options.updated_view != this) {
315 // Add missing items to the DOM.
311 // Add missing items to the DOM.
316 var items = this.model.get('value_names');
312 var items = this.model.get('value_names');
317 var that = this;
313 var that = this;
318 _.each(items, function(item, index) {
314 _.each(items, function(item, index) {
319 var item_query = ' :contains("' + item + '")';
315 var item_query = ' :contains("' + item + '")';
320 if (that.$listbox.find(item_query).length === 0) {
316 if (that.$listbox.find(item_query).length === 0) {
321 $('<option />')
317 $('<option />')
322 .text(item)
318 .text(item)
323 .attr('value_name', item)
319 .attr('value_name', item)
324 .appendTo(that.$listbox)
320 .appendTo(that.$listbox)
325 .on('click', $.proxy(that.handle_click, that));
321 .on('click', $.proxy(that.handle_click, that));
326 }
322 }
327 });
323 });
328
324
329 // Select the correct element
325 // Select the correct element
330 this.$listbox.val(this.model.get('value_name'));
326 this.$listbox.val(this.model.get('value_name'));
331
327
332 // Disable listbox if needed
328 // Disable listbox if needed
333 var disabled = this.model.get('disabled');
329 var disabled = this.model.get('disabled');
334 this.$listbox.prop('disabled', disabled);
330 this.$listbox.prop('disabled', disabled);
335
331
336 // Remove items that no longer exist.
332 // Remove items that no longer exist.
337 this.$listbox.find('option').each(function(i, obj) {
333 this.$listbox.find('option').each(function(i, obj) {
338 var value = $(obj).text();
334 var value = $(obj).text();
339 var found = false;
335 var found = false;
340 _.each(items, function(item, index) {
336 _.each(items, function(item, index) {
341 if (item == value) {
337 if (item == value) {
342 found = true;
338 found = true;
343 return false;
339 return false;
344 }
340 }
345 });
341 });
346
342
347 if (!found) {
343 if (!found) {
348 $(obj).remove();
344 $(obj).remove();
349 }
345 }
350 });
346 });
351
347
352 var description = this.model.get('description');
348 var description = this.model.get('description');
353 if (description.length === 0) {
349 if (description.length === 0) {
354 this.$label.hide();
350 this.$label.hide();
355 } else {
351 } else {
356 this.$label.text(description);
352 this.$label.text(description);
357 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
353 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
358 this.$label.show();
354 this.$label.show();
359 }
355 }
360 }
356 }
361 return SelectView.__super__.update.apply(this);
357 return SelectView.__super__.update.apply(this);
362 },
358 },
363
359
364 handle_click: function (e) {
360 handle_click: function (e) {
365 // Handle when a value is clicked.
361 // Handle when a value is clicked.
366
362
367 // Calling model.set will trigger all of the other views of the
363 // Calling model.set will trigger all of the other views of the
368 // model to update.
364 // model to update.
369 this.model.set('value_name', $(e.target).text(), {updated_view: this});
365 this.model.set('value_name', $(e.target).text(), {updated_view: this});
370 this.touch();
366 this.touch();
371 },
367 },
372 });
368 });
373
369
374 return {
370 return {
375 'DropdownView': DropdownView,
371 'DropdownView': DropdownView,
376 'RadioButtonsView': RadioButtonsView,
372 'RadioButtonsView': RadioButtonsView,
377 'ToggleButtonsView': ToggleButtonsView,
373 'ToggleButtonsView': ToggleButtonsView,
378 'SelectView': SelectView,
374 'SelectView': SelectView,
379 };
375 };
380 });
376 });
@@ -1,242 +1,240 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([
4 define([
5 "widgets/js/widget",
5 "widgets/js/widget",
6 "jquery",
6 "jquery",
7 "bootstrap",
7 "bootstrap",
8 ], function(widget, $){
8 ], function(widget, $){
9
9
10 var HTMLView = widget.DOMWidgetView.extend({
10 var HTMLView = widget.DOMWidgetView.extend({
11 render : function(){
11 render : function(){
12 // Called when view is rendered.
12 // Called when view is rendered.
13 this.update(); // Set defaults.
13 this.update(); // Set defaults.
14 },
14 },
15
15
16 update : function(){
16 update : function(){
17 // Update the contents of this view
17 // Update the contents of this view
18 //
18 //
19 // Called when the model is changed. The model may have been
19 // Called when the model is changed. The model may have been
20 // changed by another view or by a state update from the back-end.
20 // changed by another view or by a state update from the back-end.
21 this.$el.html(this.model.get('value')); // CAUTION! .html(...) CALL MANDITORY!!!
21 this.$el.html(this.model.get('value')); // CAUTION! .html(...) CALL MANDITORY!!!
22 return HTMLView.__super__.update.apply(this);
22 return HTMLView.__super__.update.apply(this);
23 },
23 },
24 });
24 });
25
25
26
26
27 var LatexView = widget.DOMWidgetView.extend({
27 var LatexView = widget.DOMWidgetView.extend({
28 render : function(){
28 render : function(){
29 // Called when view is rendered.
29 // Called when view is rendered.
30 this.update(); // Set defaults.
30 this.update(); // Set defaults.
31 },
31 },
32
32
33 update : function(){
33 update : function(){
34 // Update the contents of this view
34 // Update the contents of this view
35 //
35 //
36 // Called when the model is changed. The model may have been
36 // Called when the model is changed. The model may have been
37 // changed by another view or by a state update from the back-end.
37 // changed by another view or by a state update from the back-end.
38 this.$el.text(this.model.get('value'));
38 this.$el.text(this.model.get('value'));
39 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$el.get(0)]);
39 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$el.get(0)]);
40
40
41 return LatexView.__super__.update.apply(this);
41 return LatexView.__super__.update.apply(this);
42 },
42 },
43 });
43 });
44
44
45
45
46 var TextareaView = widget.DOMWidgetView.extend({
46 var TextareaView = widget.DOMWidgetView.extend({
47 render: function(){
47 render: function(){
48 // Called when view is rendered.
48 // Called when view is rendered.
49 this.$el
49 this.$el
50 .addClass('widget-hbox');
50 .addClass('widget-hbox');
51 this.$label = $('<div />')
51 this.$label = $('<div />')
52 .appendTo(this.$el)
52 .appendTo(this.$el)
53 .addClass('widget-hlabel')
53 .addClass('widget-hlabel')
54 .hide();
54 .hide();
55 this.$textbox = $('<textarea />')
55 this.$textbox = $('<textarea />')
56 .attr('rows', 5)
56 .attr('rows', 5)
57 .addClass('widget-text form-control')
57 .addClass('widget-text form-control')
58 .appendTo(this.$el);
58 .appendTo(this.$el);
59 this.$el_to_style = this.$textbox; // Set default element to style
60 this.update(); // Set defaults.
59 this.update(); // Set defaults.
61
60
62 this.model.on('msg:custom', $.proxy(this._handle_textarea_msg, this));
61 this.model.on('msg:custom', $.proxy(this._handle_textarea_msg, this));
63 this.model.on('change:placeholder', function(model, value, options) {
62 this.model.on('change:placeholder', function(model, value, options) {
64 this.update_placeholder(value);
63 this.update_placeholder(value);
65 }, this);
64 }, this);
66
65
67 this.update_placeholder();
66 this.update_placeholder();
68 },
67 },
69
68
70 _handle_textarea_msg: function (content){
69 _handle_textarea_msg: function (content){
71 // Handle when a custom msg is recieved from the back-end.
70 // Handle when a custom msg is recieved from the back-end.
72 if (content.method == "scroll_to_bottom") {
71 if (content.method == "scroll_to_bottom") {
73 this.scroll_to_bottom();
72 this.scroll_to_bottom();
74 }
73 }
75 },
74 },
76
75
77 update_placeholder: function(value) {
76 update_placeholder: function(value) {
78 if (!value) {
77 if (!value) {
79 value = this.model.get('placeholder');
78 value = this.model.get('placeholder');
80 }
79 }
81 this.$textbox.attr('placeholder', value);
80 this.$textbox.attr('placeholder', value);
82 },
81 },
83
82
84 scroll_to_bottom: function (){
83 scroll_to_bottom: function (){
85 // Scroll the text-area view to the bottom.
84 // Scroll the text-area view to the bottom.
86 this.$textbox.scrollTop(this.$textbox[0].scrollHeight);
85 this.$textbox.scrollTop(this.$textbox[0].scrollHeight);
87 },
86 },
88
87
89 update: function(options){
88 update: function(options){
90 // Update the contents of this view
89 // Update the contents of this view
91 //
90 //
92 // Called when the model is changed. The model may have been
91 // Called when the model is changed. The model may have been
93 // changed by another view or by a state update from the back-end.
92 // changed by another view or by a state update from the back-end.
94 if (options === undefined || options.updated_view != this) {
93 if (options === undefined || options.updated_view != this) {
95 this.$textbox.val(this.model.get('value'));
94 this.$textbox.val(this.model.get('value'));
96
95
97 var disabled = this.model.get('disabled');
96 var disabled = this.model.get('disabled');
98 this.$textbox.prop('disabled', disabled);
97 this.$textbox.prop('disabled', disabled);
99
98
100 var description = this.model.get('description');
99 var description = this.model.get('description');
101 if (description.length === 0) {
100 if (description.length === 0) {
102 this.$label.hide();
101 this.$label.hide();
103 } else {
102 } else {
104 this.$label.text(description);
103 this.$label.text(description);
105 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
104 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
106 this.$label.show();
105 this.$label.show();
107 }
106 }
108 }
107 }
109 return TextareaView.__super__.update.apply(this);
108 return TextareaView.__super__.update.apply(this);
110 },
109 },
111
110
112 events: {
111 events: {
113 // Dictionary of events and their handlers.
112 // Dictionary of events and their handlers.
114 "keyup textarea" : "handleChanging",
113 "keyup textarea" : "handleChanging",
115 "paste textarea" : "handleChanging",
114 "paste textarea" : "handleChanging",
116 "cut textarea" : "handleChanging"
115 "cut textarea" : "handleChanging"
117 },
116 },
118
117
119 handleChanging: function(e) {
118 handleChanging: function(e) {
120 // Handles and validates user input.
119 // Handles and validates user input.
121
120
122 // Calling model.set will trigger all of the other views of the
121 // Calling model.set will trigger all of the other views of the
123 // model to update.
122 // model to update.
124 this.model.set('value', e.target.value, {updated_view: this});
123 this.model.set('value', e.target.value, {updated_view: this});
125 this.touch();
124 this.touch();
126 },
125 },
127 });
126 });
128
127
129
128
130 var TextView = widget.DOMWidgetView.extend({
129 var TextView = widget.DOMWidgetView.extend({
131 render: function(){
130 render: function(){
132 // Called when view is rendered.
131 // Called when view is rendered.
133 this.$el
132 this.$el
134 .addClass('widget-hbox-single');
133 .addClass('widget-hbox-single');
135 this.$label = $('<div />')
134 this.$label = $('<div />')
136 .addClass('widget-hlabel')
135 .addClass('widget-hlabel')
137 .appendTo(this.$el)
136 .appendTo(this.$el)
138 .hide();
137 .hide();
139 this.$textbox = $('<input type="text" />')
138 this.$textbox = $('<input type="text" />')
140 .addClass('input')
139 .addClass('input')
141 .addClass('widget-text form-control')
140 .addClass('widget-text form-control')
142 .appendTo(this.$el);
141 .appendTo(this.$el);
143 this.$el_to_style = this.$textbox; // Set default element to style
144 this.update(); // Set defaults.
142 this.update(); // Set defaults.
145 this.model.on('change:placeholder', function(model, value, options) {
143 this.model.on('change:placeholder', function(model, value, options) {
146 this.update_placeholder(value);
144 this.update_placeholder(value);
147 }, this);
145 }, this);
148
146
149 this.update_placeholder();
147 this.update_placeholder();
150 },
148 },
151
149
152 update_placeholder: function(value) {
150 update_placeholder: function(value) {
153 if (!value) {
151 if (!value) {
154 value = this.model.get('placeholder');
152 value = this.model.get('placeholder');
155 }
153 }
156 this.$textbox.attr('placeholder', value);
154 this.$textbox.attr('placeholder', value);
157 },
155 },
158
156
159 update: function(options){
157 update: function(options){
160 // Update the contents of this view
158 // Update the contents of this view
161 //
159 //
162 // Called when the model is changed. The model may have been
160 // Called when the model is changed. The model may have been
163 // changed by another view or by a state update from the back-end.
161 // changed by another view or by a state update from the back-end.
164 if (options === undefined || options.updated_view != this) {
162 if (options === undefined || options.updated_view != this) {
165 if (this.$textbox.val() != this.model.get('value')) {
163 if (this.$textbox.val() != this.model.get('value')) {
166 this.$textbox.val(this.model.get('value'));
164 this.$textbox.val(this.model.get('value'));
167 }
165 }
168
166
169 var disabled = this.model.get('disabled');
167 var disabled = this.model.get('disabled');
170 this.$textbox.prop('disabled', disabled);
168 this.$textbox.prop('disabled', disabled);
171
169
172 var description = this.model.get('description');
170 var description = this.model.get('description');
173 if (description.length === 0) {
171 if (description.length === 0) {
174 this.$label.hide();
172 this.$label.hide();
175 } else {
173 } else {
176 this.$label.text(description);
174 this.$label.text(description);
177 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
175 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
178 this.$label.show();
176 this.$label.show();
179 }
177 }
180 }
178 }
181 return TextView.__super__.update.apply(this);
179 return TextView.__super__.update.apply(this);
182 },
180 },
183
181
184 events: {
182 events: {
185 // Dictionary of events and their handlers.
183 // Dictionary of events and their handlers.
186 "keyup input" : "handleChanging",
184 "keyup input" : "handleChanging",
187 "paste input" : "handleChanging",
185 "paste input" : "handleChanging",
188 "cut input" : "handleChanging",
186 "cut input" : "handleChanging",
189 "keypress input" : "handleKeypress",
187 "keypress input" : "handleKeypress",
190 "blur input" : "handleBlur",
188 "blur input" : "handleBlur",
191 "focusout input" : "handleFocusOut"
189 "focusout input" : "handleFocusOut"
192 },
190 },
193
191
194 handleChanging: function(e) {
192 handleChanging: function(e) {
195 // Handles user input.
193 // Handles user input.
196
194
197 // Calling model.set will trigger all of the other views of the
195 // Calling model.set will trigger all of the other views of the
198 // model to update.
196 // model to update.
199 this.model.set('value', e.target.value, {updated_view: this});
197 this.model.set('value', e.target.value, {updated_view: this});
200 this.touch();
198 this.touch();
201 },
199 },
202
200
203 handleKeypress: function(e) {
201 handleKeypress: function(e) {
204 // Handles text submition
202 // Handles text submition
205 if (e.keyCode == 13) { // Return key
203 if (e.keyCode == 13) { // Return key
206 this.send({event: 'submit'});
204 this.send({event: 'submit'});
207 event.stopPropagation();
205 event.stopPropagation();
208 event.preventDefault();
206 event.preventDefault();
209 return false;
207 return false;
210 }
208 }
211 },
209 },
212
210
213 handleBlur: function(e) {
211 handleBlur: function(e) {
214 // Prevent a blur from firing if the blur was not user intended.
212 // Prevent a blur from firing if the blur was not user intended.
215 // This is a workaround for the return-key focus loss bug.
213 // This is a workaround for the return-key focus loss bug.
216 // TODO: Is the original bug actually a fault of the keyboard
214 // TODO: Is the original bug actually a fault of the keyboard
217 // manager?
215 // manager?
218 if (e.relatedTarget === null) {
216 if (e.relatedTarget === null) {
219 event.stopPropagation();
217 event.stopPropagation();
220 event.preventDefault();
218 event.preventDefault();
221 return false;
219 return false;
222 }
220 }
223 },
221 },
224
222
225 handleFocusOut: function(e) {
223 handleFocusOut: function(e) {
226 // Prevent a blur from firing if the blur was not user intended.
224 // Prevent a blur from firing if the blur was not user intended.
227 // This is a workaround for the return-key focus loss bug.
225 // This is a workaround for the return-key focus loss bug.
228 if (e.relatedTarget === null) {
226 if (e.relatedTarget === null) {
229 event.stopPropagation();
227 event.stopPropagation();
230 event.preventDefault();
228 event.preventDefault();
231 return false;
229 return false;
232 }
230 }
233 },
231 },
234 });
232 });
235
233
236 return {
234 return {
237 'HTMLView': HTMLView,
235 'HTMLView': HTMLView,
238 'LatexView': LatexView,
236 'LatexView': LatexView,
239 'TextareaView': TextareaView,
237 'TextareaView': TextareaView,
240 'TextView': TextView,
238 'TextView': TextView,
241 };
239 };
242 });
240 });
@@ -1,188 +1,188 b''
1 var xor = function (a, b) {return !a ^ !b;};
1 var xor = function (a, b) {return !a ^ !b;};
2 var isArray = function (a) {
2 var isArray = function (a) {
3 try {
3 try {
4 return Object.toString.call(a) === "[object Array]" || Object.toString.call(a) === "[object RuntimeArray]";
4 return Object.toString.call(a) === "[object Array]" || Object.toString.call(a) === "[object RuntimeArray]";
5 } catch (e) {
5 } catch (e) {
6 return Array.isArray(a);
6 return Array.isArray(a);
7 }
7 }
8 };
8 };
9 var recursive_compare = function(a, b) {
9 var recursive_compare = function(a, b) {
10 // Recursively compare two objects.
10 // Recursively compare two objects.
11 var same = true;
11 var same = true;
12 same = same && !xor(a instanceof Object || typeof a == 'object', b instanceof Object || typeof b == 'object');
12 same = same && !xor(a instanceof Object || typeof a == 'object', b instanceof Object || typeof b == 'object');
13 same = same && !xor(isArray(a), isArray(b));
13 same = same && !xor(isArray(a), isArray(b));
14
14
15 if (same) {
15 if (same) {
16 if (a instanceof Object) {
16 if (a instanceof Object) {
17 var key;
17 var key;
18 for (key in a) {
18 for (key in a) {
19 if (a.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
19 if (a.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
20 same = false;
20 same = false;
21 break;
21 break;
22 }
22 }
23 }
23 }
24 for (key in b) {
24 for (key in b) {
25 if (b.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
25 if (b.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
26 same = false;
26 same = false;
27 break;
27 break;
28 }
28 }
29 }
29 }
30 } else {
30 } else {
31 return a === b;
31 return a === b;
32 }
32 }
33 }
33 }
34
34
35 return same;
35 return same;
36 };
36 };
37
37
38 // Test the widget framework.
38 // Test the widget framework.
39 casper.notebook_test(function () {
39 casper.notebook_test(function () {
40 var index;
40 var index;
41
41
42 this.then(function () {
42 this.then(function () {
43
43
44 // Check if the WidgetManager class is defined.
44 // Check if the WidgetManager class is defined.
45 this.test.assert(this.evaluate(function() {
45 this.test.assert(this.evaluate(function() {
46 return IPython.WidgetManager !== undefined;
46 return IPython.WidgetManager !== undefined;
47 }), 'WidgetManager class is defined');
47 }), 'WidgetManager class is defined');
48 });
48 });
49
49
50 index = this.append_cell(
50 index = this.append_cell(
51 'from IPython.html import widgets\n' +
51 'from IPython.html import widgets\n' +
52 'from IPython.display import display, clear_output\n' +
52 'from IPython.display import display, clear_output\n' +
53 'print("Success")');
53 'print("Success")');
54 this.execute_cell_then(index);
54 this.execute_cell_then(index);
55
55
56 this.then(function () {
56 this.then(function () {
57 // Check if the widget manager has been instantiated.
57 // Check if the widget manager has been instantiated.
58 this.test.assert(this.evaluate(function() {
58 this.test.assert(this.evaluate(function() {
59 return IPython.notebook.kernel.widget_manager !== undefined;
59 return IPython.notebook.kernel.widget_manager !== undefined;
60 }), 'Notebook widget manager instantiated');
60 }), 'Notebook widget manager instantiated');
61
61
62 // Functions that can be used to test the packing and unpacking APIs
62 // Functions that can be used to test the packing and unpacking APIs
63 var that = this;
63 var that = this;
64 var test_pack = function (input) {
64 var test_pack = function (input) {
65 var output = that.evaluate(function(input) {
65 var output = that.evaluate(function(input) {
66 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
66 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
67 var results = model._pack_models(input);
67 var results = model._pack_models(input);
68 return results;
68 return results;
69 }, {input: input});
69 }, {input: input});
70 that.test.assert(recursive_compare(input, output),
70 that.test.assert(recursive_compare(input, output),
71 JSON.stringify(input) + ' passed through Model._pack_model unchanged');
71 JSON.stringify(input) + ' passed through Model._pack_model unchanged');
72 };
72 };
73 var test_unpack = function (input) {
73 var test_unpack = function (input) {
74 var output = that.evaluate(function(input) {
74 var output = that.evaluate(function(input) {
75 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
75 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
76 var results = model._unpack_models(input);
76 var results = model._unpack_models(input);
77 return results;
77 return results;
78 }, {input: input});
78 }, {input: input});
79 that.test.assert(recursive_compare(input, output),
79 that.test.assert(recursive_compare(input, output),
80 JSON.stringify(input) + ' passed through Model._unpack_model unchanged');
80 JSON.stringify(input) + ' passed through Model._unpack_model unchanged');
81 };
81 };
82 var test_packing = function(input) {
82 var test_packing = function(input) {
83 test_pack(input);
83 test_pack(input);
84 test_unpack(input);
84 test_unpack(input);
85 };
85 };
86
86
87 test_packing({0: 'hi', 1: 'bye'});
87 test_packing({0: 'hi', 1: 'bye'});
88 test_packing(['hi', 'bye']);
88 test_packing(['hi', 'bye']);
89 test_packing(['hi', 5]);
89 test_packing(['hi', 5]);
90 test_packing(['hi', '5']);
90 test_packing(['hi', '5']);
91 test_packing([1.0, 0]);
91 test_packing([1.0, 0]);
92 test_packing([1.0, false]);
92 test_packing([1.0, false]);
93 test_packing([1, false]);
93 test_packing([1, false]);
94 test_packing([1, false, {a: 'hi'}]);
94 test_packing([1, false, {a: 'hi'}]);
95 test_packing([1, false, ['hi']]);
95 test_packing([1, false, ['hi']]);
96
96
97 // Test multi-set, single touch code. First create a custom widget.
97 // Test multi-set, single touch code. First create a custom widget.
98 this.evaluate(function() {
98 this.evaluate(function() {
99 var MultiSetView = IPython.DOMWidgetView.extend({
99 var MultiSetView = IPython.DOMWidgetView.extend({
100 render: function(){
100 render: function(){
101 this.model.set('a', 1);
101 this.model.set('a', 1);
102 this.model.set('b', 2);
102 this.model.set('b', 2);
103 this.model.set('c', 3);
103 this.model.set('c', 3);
104 this.touch();
104 this.touch();
105 },
105 },
106 });
106 });
107 IPython.WidgetManager.register_widget_view('MultiSetView', MultiSetView);
107 IPython.WidgetManager.register_widget_view('MultiSetView', MultiSetView);
108 }, {});
108 }, {});
109 });
109 });
110
110
111 // Try creating the multiset widget, verify that sets the values correctly.
111 // Try creating the multiset widget, verify that sets the values correctly.
112 var multiset = {};
112 var multiset = {};
113 multiset.index = this.append_cell(
113 multiset.index = this.append_cell(
114 'from IPython.utils.traitlets import Unicode, CInt\n' +
114 'from IPython.utils.traitlets import Unicode, CInt\n' +
115 'class MultiSetWidget(widgets.Widget):\n' +
115 'class MultiSetWidget(widgets.Widget):\n' +
116 ' _view_name = Unicode("MultiSetView", sync=True)\n' +
116 ' _view_name = Unicode("MultiSetView", sync=True)\n' +
117 ' a = CInt(0, sync=True)\n' +
117 ' a = CInt(0, sync=True)\n' +
118 ' b = CInt(0, sync=True)\n' +
118 ' b = CInt(0, sync=True)\n' +
119 ' c = CInt(0, sync=True)\n' +
119 ' c = CInt(0, sync=True)\n' +
120 ' d = CInt(-1, sync=True)\n' + // See if it sends a full state.
120 ' d = CInt(-1, sync=True)\n' + // See if it sends a full state.
121 ' def _handle_receive_state(self, sync_data):\n' +
121 ' def _handle_receive_state(self, sync_data):\n' +
122 ' widgets.Widget._handle_receive_state(self, sync_data)\n'+
122 ' widgets.Widget._handle_receive_state(self, sync_data)\n'+
123 ' self.d = len(sync_data)\n' +
123 ' self.d = len(sync_data)\n' +
124 'multiset = MultiSetWidget()\n' +
124 'multiset = MultiSetWidget()\n' +
125 'display(multiset)\n' +
125 'display(multiset)\n' +
126 'print(multiset.model_id)');
126 'print(multiset.model_id)');
127 this.execute_cell_then(multiset.index, function(index) {
127 this.execute_cell_then(multiset.index, function(index) {
128 multiset.model_id = this.get_output_cell(index).text.trim();
128 multiset.model_id = this.get_output_cell(index).text.trim();
129 });
129 });
130
130
131 this.wait_for_widget(multiset);
131 this.wait_for_widget(multiset);
132
132
133 index = this.append_cell(
133 index = this.append_cell(
134 'print("%d%d%d" % (multiset.a, multiset.b, multiset.c))');
134 'print("%d%d%d" % (multiset.a, multiset.b, multiset.c))');
135 this.execute_cell_then(index, function(index) {
135 this.execute_cell_then(index, function(index) {
136 this.test.assertEquals(this.get_output_cell(index).text.trim(), '123',
136 this.test.assertEquals(this.get_output_cell(index).text.trim(), '123',
137 'Multiple model.set calls and one view.touch update state in back-end.');
137 'Multiple model.set calls and one view.touch update state in back-end.');
138 });
138 });
139
139
140 index = this.append_cell(
140 index = this.append_cell(
141 'print("%d" % (multiset.d))');
141 'print("%d" % (multiset.d))');
142 this.execute_cell_then(index, function(index) {
142 this.execute_cell_then(index, function(index) {
143 this.test.assertEquals(this.get_output_cell(index).text.trim(), '3',
143 this.test.assertEquals(this.get_output_cell(index).text.trim(), '3',
144 'Multiple model.set calls sent a partial state.');
144 'Multiple model.set calls sent a partial state.');
145 });
145 });
146
146
147 var textbox = {};
147 var textbox = {};
148 throttle_index = this.append_cell(
148 throttle_index = this.append_cell(
149 'import time\n' +
149 'import time\n' +
150 'textbox = widgets.TextWidget()\n' +
150 'textbox = widgets.TextWidget()\n' +
151 'display(textbox)\n' +
151 'display(textbox)\n' +
152 'textbox.add_class("my-throttle-textbox")\n' +
152 'textbox.add_class("my-throttle-textbox", selector="input")\n' +
153 'def handle_change(name, old, new):\n' +
153 'def handle_change(name, old, new):\n' +
154 ' display(len(new))\n' +
154 ' display(len(new))\n' +
155 ' time.sleep(0.5)\n' +
155 ' time.sleep(0.5)\n' +
156 'textbox.on_trait_change(handle_change, "value")\n' +
156 'textbox.on_trait_change(handle_change, "value")\n' +
157 'print(textbox.model_id)');
157 'print(textbox.model_id)');
158 this.execute_cell_then(throttle_index, function(index){
158 this.execute_cell_then(throttle_index, function(index){
159 textbox.model_id = this.get_output_cell(index).text.trim();
159 textbox.model_id = this.get_output_cell(index).text.trim();
160
160
161 this.test.assert(this.cell_element_exists(index,
161 this.test.assert(this.cell_element_exists(index,
162 '.widget-area .widget-subarea'),
162 '.widget-area .widget-subarea'),
163 'Widget subarea exists.');
163 'Widget subarea exists.');
164
164
165 this.test.assert(this.cell_element_exists(index,
165 this.test.assert(this.cell_element_exists(index,
166 '.my-throttle-textbox'), 'Textbox exists.');
166 '.my-throttle-textbox'), 'Textbox exists.');
167
167
168 // Send 20 characters
168 // Send 20 characters
169 this.sendKeys('.my-throttle-textbox', '....................');
169 this.sendKeys('.my-throttle-textbox', '....................');
170 });
170 });
171
171
172 this.wait_for_widget(textbox);
172 this.wait_for_widget(textbox);
173
173
174 this.then(function () {
174 this.then(function () {
175 var outputs = this.evaluate(function(i) {
175 var outputs = this.evaluate(function(i) {
176 return IPython.notebook.get_cell(i).output_area.outputs;
176 return IPython.notebook.get_cell(i).output_area.outputs;
177 }, {i : throttle_index});
177 }, {i : throttle_index});
178
178
179 // Only 4 outputs should have printed, but because of timing, sometimes
179 // Only 4 outputs should have printed, but because of timing, sometimes
180 // 5 outputs will print. All we need to do is verify num outputs <= 5
180 // 5 outputs will print. All we need to do is verify num outputs <= 5
181 // because that is much less than 20.
181 // because that is much less than 20.
182 this.test.assert(outputs.length <= 5, 'Messages throttled.');
182 this.test.assert(outputs.length <= 5, 'Messages throttled.');
183
183
184 // We also need to verify that the last state sent was correct.
184 // We also need to verify that the last state sent was correct.
185 var last_state = outputs[outputs.length-1]['text/plain'];
185 var last_state = outputs[outputs.length-1]['text/plain'];
186 this.test.assertEquals(last_state, "20", "Last state sent when throttling.");
186 this.test.assertEquals(last_state, "20", "Last state sent when throttling.");
187 });
187 });
188 });
188 });
@@ -1,100 +1,100 b''
1 // Test widget float class
1 // Test widget float class
2 casper.notebook_test(function () {
2 casper.notebook_test(function () {
3 index = this.append_cell(
3 index = this.append_cell(
4 'from IPython.html import widgets\n' +
4 'from IPython.html import widgets\n' +
5 'from IPython.display import display, clear_output\n' +
5 'from IPython.display import display, clear_output\n' +
6 'print("Success")');
6 'print("Success")');
7 this.execute_cell_then(index);
7 this.execute_cell_then(index);
8
8
9 var float_text = {};
9 var float_text = {};
10 float_text.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-float-text';
10 float_text.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-float-text';
11 float_text.index = this.append_cell(
11 float_text.index = this.append_cell(
12 'float_widget = widgets.FloatTextWidget()\n' +
12 'float_widget = widgets.FloatTextWidget()\n' +
13 'display(float_widget)\n' +
13 'display(float_widget)\n' +
14 'float_widget.add_class("my-second-float-text")\n' +
14 'float_widget.add_class("my-second-float-text", selector="input")\n' +
15 'print(float_widget.model_id)\n');
15 'print(float_widget.model_id)\n');
16 this.execute_cell_then(float_text.index, function(index){
16 this.execute_cell_then(float_text.index, function(index){
17 float_text.model_id = this.get_output_cell(index).text.trim();
17 float_text.model_id = this.get_output_cell(index).text.trim();
18
18
19 this.test.assert(this.cell_element_exists(index,
19 this.test.assert(this.cell_element_exists(index,
20 '.widget-area .widget-subarea'),
20 '.widget-area .widget-subarea'),
21 'Widget subarea exists.');
21 'Widget subarea exists.');
22
22
23 this.test.assert(this.cell_element_exists(index, float_text.query),
23 this.test.assert(this.cell_element_exists(index, float_text.query),
24 'Widget float textbox exists.');
24 'Widget float textbox exists.');
25
25
26 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
26 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
27 this.sendKeys(float_text.query, '1.05');
27 this.sendKeys(float_text.query, '1.05');
28 });
28 });
29
29
30 this.wait_for_widget(float_text);
30 this.wait_for_widget(float_text);
31
31
32 index = this.append_cell('print(float_widget.value)\n');
32 index = this.append_cell('print(float_widget.value)\n');
33 this.execute_cell_then(index, function(index){
33 this.execute_cell_then(index, function(index){
34 this.test.assertEquals(this.get_output_cell(index).text, '1.05\n',
34 this.test.assertEquals(this.get_output_cell(index).text, '1.05\n',
35 'Float textbox value set.');
35 'Float textbox value set.');
36 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
36 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
37 this.sendKeys(float_text.query, '123456789.0');
37 this.sendKeys(float_text.query, '123456789.0');
38 });
38 });
39
39
40 this.wait_for_widget(float_text);
40 this.wait_for_widget(float_text);
41
41
42 index = this.append_cell('print(float_widget.value)\n');
42 index = this.append_cell('print(float_widget.value)\n');
43 this.execute_cell_then(index, function(index){
43 this.execute_cell_then(index, function(index){
44 this.test.assertEquals(this.get_output_cell(index).text, '123456789.0\n',
44 this.test.assertEquals(this.get_output_cell(index).text, '123456789.0\n',
45 'Long float textbox value set (probably triggers throttling).');
45 'Long float textbox value set (probably triggers throttling).');
46 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
46 this.cell_element_function(float_text.index, float_text.query, 'val', ['']);
47 this.sendKeys(float_text.query, '12hello');
47 this.sendKeys(float_text.query, '12hello');
48 });
48 });
49
49
50 this.wait_for_widget(float_text);
50 this.wait_for_widget(float_text);
51
51
52 index = this.append_cell('print(float_widget.value)\n');
52 index = this.append_cell('print(float_widget.value)\n');
53 this.execute_cell_then(index, function(index){
53 this.execute_cell_then(index, function(index){
54 this.test.assertEquals(this.get_output_cell(index).text, '12.0\n',
54 this.test.assertEquals(this.get_output_cell(index).text, '12.0\n',
55 'Invald float textbox value caught and filtered.');
55 'Invald float textbox value caught and filtered.');
56 });
56 });
57
57
58 var float_text_query = '.widget-area .widget-subarea .widget-hbox-single .widget-numeric-text';
58 var float_text_query = '.widget-area .widget-subarea .widget-hbox-single .widget-numeric-text';
59 var slider = {};
59 var slider = {};
60 slider.query = '.widget-area .widget-subarea .widget-hbox-single .slider';
60 slider.query = '.widget-area .widget-subarea .widget-hbox-single .slider';
61 slider.index = this.append_cell(
61 slider.index = this.append_cell(
62 'floatrange = [widgets.BoundedFloatTextWidget(), \n' +
62 'floatrange = [widgets.BoundedFloatTextWidget(), \n' +
63 ' widgets.FloatSliderWidget()]\n' +
63 ' widgets.FloatSliderWidget()]\n' +
64 '[display(floatrange[i]) for i in range(2)]\n' +
64 '[display(floatrange[i]) for i in range(2)]\n' +
65 'print("Success")\n');
65 'print("Success")\n');
66 this.execute_cell_then(slider.index, function(index){
66 this.execute_cell_then(slider.index, function(index){
67
67
68 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
68 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
69 'Create float range cell executed with correct output.');
69 'Create float range cell executed with correct output.');
70
70
71 this.test.assert(this.cell_element_exists(index,
71 this.test.assert(this.cell_element_exists(index,
72 '.widget-area .widget-subarea'),
72 '.widget-area .widget-subarea'),
73 'Widget subarea exists.');
73 'Widget subarea exists.');
74
74
75 this.test.assert(this.cell_element_exists(index, slider.query),
75 this.test.assert(this.cell_element_exists(index, slider.query),
76 'Widget slider exists.');
76 'Widget slider exists.');
77
77
78 this.test.assert(this.cell_element_exists(index, float_text_query),
78 this.test.assert(this.cell_element_exists(index, float_text_query),
79 'Widget float textbox exists.');
79 'Widget float textbox exists.');
80 });
80 });
81
81
82 index = this.append_cell(
82 index = this.append_cell(
83 'for widget in floatrange:\n' +
83 'for widget in floatrange:\n' +
84 ' widget.max = 50.0\n' +
84 ' widget.max = 50.0\n' +
85 ' widget.min = -50.0\n' +
85 ' widget.min = -50.0\n' +
86 ' widget.value = 25.0\n' +
86 ' widget.value = 25.0\n' +
87 'print("Success")\n');
87 'print("Success")\n');
88 this.execute_cell_then(index, function(index){
88 this.execute_cell_then(index, function(index){
89
89
90 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
90 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
91 'Float range properties cell executed with correct output.');
91 'Float range properties cell executed with correct output.');
92
92
93 this.test.assert(this.cell_element_exists(slider.index, slider.query),
93 this.test.assert(this.cell_element_exists(slider.index, slider.query),
94 'Widget slider exists.');
94 'Widget slider exists.');
95
95
96 this.test.assert(this.cell_element_function(slider.index, slider.query,
96 this.test.assert(this.cell_element_function(slider.index, slider.query,
97 'slider', ['value']) == 25.0,
97 'slider', ['value']) == 25.0,
98 'Slider set to Python value.');
98 'Slider set to Python value.');
99 });
99 });
100 }); No newline at end of file
100 });
@@ -1,157 +1,157 b''
1 // Test widget int class
1 // Test widget int class
2 casper.notebook_test(function () {
2 casper.notebook_test(function () {
3 index = this.append_cell(
3 index = this.append_cell(
4 'from IPython.html import widgets\n' +
4 'from IPython.html import widgets\n' +
5 'from IPython.display import display, clear_output\n' +
5 'from IPython.display import display, clear_output\n' +
6 'print("Success")');
6 'print("Success")');
7 this.execute_cell_then(index);
7 this.execute_cell_then(index);
8
8
9 var int_text = {}
9 var int_text = {};
10 int_text.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-int-text';
10 int_text.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-int-text';
11 int_text.index = this.append_cell(
11 int_text.index = this.append_cell(
12 'int_widget = widgets.IntTextWidget()\n' +
12 'int_widget = widgets.IntTextWidget()\n' +
13 'display(int_widget)\n' +
13 'display(int_widget)\n' +
14 'int_widget.add_class("my-second-int-text")\n' +
14 'int_widget.add_class("my-second-int-text", selector="input")\n' +
15 'print(int_widget.model_id)\n');
15 'print(int_widget.model_id)\n');
16 this.execute_cell_then(int_text.index, function(index){
16 this.execute_cell_then(int_text.index, function(index){
17 int_text.model_id = this.get_output_cell(index).text.trim();
17 int_text.model_id = this.get_output_cell(index).text.trim();
18
18
19 this.test.assert(this.cell_element_exists(index,
19 this.test.assert(this.cell_element_exists(index,
20 '.widget-area .widget-subarea'),
20 '.widget-area .widget-subarea'),
21 'Widget subarea exists.');
21 'Widget subarea exists.');
22
22
23 this.test.assert(this.cell_element_exists(index, int_text.query),
23 this.test.assert(this.cell_element_exists(index, int_text.query),
24 'Widget int textbox exists.');
24 'Widget int textbox exists.');
25
25
26 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
26 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
27 this.sendKeys(int_text.query, '1.05');
27 this.sendKeys(int_text.query, '1.05');
28 });
28 });
29
29
30 this.wait_for_widget(int_text);
30 this.wait_for_widget(int_text);
31
31
32 index = this.append_cell('print(int_widget.value)\n');
32 index = this.append_cell('print(int_widget.value)\n');
33 this.execute_cell_then(index, function(index){
33 this.execute_cell_then(index, function(index){
34 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
34 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
35 'Int textbox value set.');
35 'Int textbox value set.');
36 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
36 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
37 this.sendKeys(int_text.query, '123456789');
37 this.sendKeys(int_text.query, '123456789');
38 });
38 });
39
39
40 this.wait_for_widget(int_text);
40 this.wait_for_widget(int_text);
41
41
42 index = this.append_cell('print(int_widget.value)\n');
42 index = this.append_cell('print(int_widget.value)\n');
43 this.execute_cell_then(index, function(index){
43 this.execute_cell_then(index, function(index){
44 this.test.assertEquals(this.get_output_cell(index).text, '123456789\n',
44 this.test.assertEquals(this.get_output_cell(index).text, '123456789\n',
45 'Long int textbox value set (probably triggers throttling).');
45 'Long int textbox value set (probably triggers throttling).');
46 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
46 this.cell_element_function(int_text.index, int_text.query, 'val', ['']);
47 this.sendKeys(int_text.query, '12hello');
47 this.sendKeys(int_text.query, '12hello');
48 });
48 });
49
49
50 this.wait_for_widget(int_text);
50 this.wait_for_widget(int_text);
51
51
52 index = this.append_cell('print(int_widget.value)\n');
52 index = this.append_cell('print(int_widget.value)\n');
53 this.execute_cell_then(index, function(index){
53 this.execute_cell_then(index, function(index){
54 this.test.assertEquals(this.get_output_cell(index).text, '12\n',
54 this.test.assertEquals(this.get_output_cell(index).text, '12\n',
55 'Invald int textbox value caught and filtered.');
55 'Invald int textbox value caught and filtered.');
56 });
56 });
57
57
58 index = this.append_cell(
58 index = this.append_cell(
59 'from IPython.html import widgets\n' +
59 'from IPython.html import widgets\n' +
60 'from IPython.display import display, clear_output\n' +
60 'from IPython.display import display, clear_output\n' +
61 'print("Success")');
61 'print("Success")');
62 this.execute_cell_then(index);
62 this.execute_cell_then(index);
63
63
64
64
65 var slider_query = '.widget-area .widget-subarea .widget-hbox-single .slider';
65 var slider_query = '.widget-area .widget-subarea .widget-hbox-single .slider';
66 var int_text2 = {};
66 var int_text2 = {};
67 int_text2.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-num-test-text';
67 int_text2.query = '.widget-area .widget-subarea .widget-hbox-single .my-second-num-test-text';
68 int_text2.index = this.append_cell(
68 int_text2.index = this.append_cell(
69 'intrange = [widgets.BoundedIntTextWidget(),\n' +
69 'intrange = [widgets.BoundedIntTextWidget(),\n' +
70 ' widgets.IntSliderWidget()]\n' +
70 ' widgets.IntSliderWidget()]\n' +
71 '[display(intrange[i]) for i in range(2)]\n' +
71 '[display(intrange[i]) for i in range(2)]\n' +
72 'intrange[0].add_class("my-second-num-test-text")\n' +
72 'intrange[0].add_class("my-second-num-test-text", selector="input")\n' +
73 'print(intrange[0].model_id)\n');
73 'print(intrange[0].model_id)\n');
74 this.execute_cell_then(int_text2.index, function(index){
74 this.execute_cell_then(int_text2.index, function(index){
75 int_text2.model_id = this.get_output_cell(index).text.trim();
75 int_text2.model_id = this.get_output_cell(index).text.trim();
76
76
77 this.test.assert(this.cell_element_exists(index,
77 this.test.assert(this.cell_element_exists(index,
78 '.widget-area .widget-subarea'),
78 '.widget-area .widget-subarea'),
79 'Widget subarea exists.');
79 'Widget subarea exists.');
80
80
81 this.test.assert(this.cell_element_exists(index, slider_query),
81 this.test.assert(this.cell_element_exists(index, slider_query),
82 'Widget slider exists.');
82 'Widget slider exists.');
83
83
84 this.test.assert(this.cell_element_exists(index, int_text2.query),
84 this.test.assert(this.cell_element_exists(index, int_text2.query),
85 'Widget int textbox exists.');
85 'Widget int textbox exists.');
86 });
86 });
87
87
88 index = this.append_cell(
88 index = this.append_cell(
89 'for widget in intrange:\n' +
89 'for widget in intrange:\n' +
90 ' widget.max = 50\n' +
90 ' widget.max = 50\n' +
91 ' widget.min = -50\n' +
91 ' widget.min = -50\n' +
92 ' widget.value = 25\n' +
92 ' widget.value = 25\n' +
93 'print("Success")\n');
93 'print("Success")\n');
94 this.execute_cell_then(index, function(index){
94 this.execute_cell_then(index, function(index){
95
95
96 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
96 this.test.assertEquals(this.get_output_cell(index).text, 'Success\n',
97 'Int range properties cell executed with correct output.');
97 'Int range properties cell executed with correct output.');
98
98
99 this.test.assert(this.cell_element_exists(int_text2.index, slider_query),
99 this.test.assert(this.cell_element_exists(int_text2.index, slider_query),
100 'Widget slider exists.');
100 'Widget slider exists.');
101
101
102 this.test.assert(this.cell_element_function(int_text2.index, slider_query,
102 this.test.assert(this.cell_element_function(int_text2.index, slider_query,
103 'slider', ['value']) == 25,
103 'slider', ['value']) == 25,
104 'Slider set to Python value.');
104 'Slider set to Python value.');
105
105
106 this.test.assert(this.cell_element_function(int_text2.index, int_text2.query,
106 this.test.assert(this.cell_element_function(int_text2.index, int_text2.query,
107 'val') == 25, 'Int textbox set to Python value.');
107 'val') == 25, 'Int textbox set to Python value.');
108
108
109 // Clear the int textbox value and then set it to 1 by emulating
109 // Clear the int textbox value and then set it to 1 by emulating
110 // keyboard presses.
110 // keyboard presses.
111 this.evaluate(function(q){
111 this.evaluate(function(q){
112 var textbox = IPython.notebook.element.find(q);
112 var textbox = IPython.notebook.element.find(q);
113 textbox.val('1');
113 textbox.val('1');
114 textbox.trigger('keyup');
114 textbox.trigger('keyup');
115 }, {q: int_text2.query});
115 }, {q: int_text2.query});
116 });
116 });
117
117
118 this.wait_for_widget(int_text2);
118 this.wait_for_widget(int_text2);
119
119
120 index = this.append_cell('print(intrange[0].value)\n');
120 index = this.append_cell('print(intrange[0].value)\n');
121 this.execute_cell_then(index, function(index){
121 this.execute_cell_then(index, function(index){
122 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
122 this.test.assertEquals(this.get_output_cell(index).text, '1\n',
123 'Int textbox set int range value');
123 'Int textbox set int range value');
124
124
125 // Clear the int textbox value and then set it to 120 by emulating
125 // Clear the int textbox value and then set it to 120 by emulating
126 // keyboard presses.
126 // keyboard presses.
127 this.evaluate(function(q){
127 this.evaluate(function(q){
128 var textbox = IPython.notebook.element.find(q);
128 var textbox = IPython.notebook.element.find(q);
129 textbox.val('120');
129 textbox.val('120');
130 textbox.trigger('keyup');
130 textbox.trigger('keyup');
131 }, {q: int_text2.query});
131 }, {q: int_text2.query});
132 });
132 });
133
133
134 this.wait_for_widget(int_text2);
134 this.wait_for_widget(int_text2);
135
135
136 index = this.append_cell('print(intrange[0].value)\n');
136 index = this.append_cell('print(intrange[0].value)\n');
137 this.execute_cell_then(index, function(index){
137 this.execute_cell_then(index, function(index){
138 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
138 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
139 'Int textbox value bound');
139 'Int textbox value bound');
140
140
141 // Clear the int textbox value and then set it to 'hello world' by
141 // Clear the int textbox value and then set it to 'hello world' by
142 // emulating keyboard presses. 'hello world' should get filtered...
142 // emulating keyboard presses. 'hello world' should get filtered...
143 this.evaluate(function(q){
143 this.evaluate(function(q){
144 var textbox = IPython.notebook.element.find(q);
144 var textbox = IPython.notebook.element.find(q);
145 textbox.val('hello world');
145 textbox.val('hello world');
146 textbox.trigger('keyup');
146 textbox.trigger('keyup');
147 }, {q: int_text2.query});
147 }, {q: int_text2.query});
148 });
148 });
149
149
150 this.wait_for_widget(int_text2);
150 this.wait_for_widget(int_text2);
151
151
152 index = this.append_cell('print(intrange[0].value)\n');
152 index = this.append_cell('print(intrange[0].value)\n');
153 this.execute_cell_then(index, function(index){
153 this.execute_cell_then(index, function(index){
154 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
154 this.test.assertEquals(this.get_output_cell(index).text, '50\n',
155 'Invalid int textbox characters ignored');
155 'Invalid int textbox characters ignored');
156 });
156 });
157 }); No newline at end of file
157 });
@@ -1,449 +1,446 b''
1 """Base Widget class. Allows user to create widgets in the back-end that render
1 """Base Widget class. Allows user to create widgets in the back-end that render
2 in the IPython notebook front-end.
2 in the IPython notebook front-end.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from contextlib import contextmanager
15 from contextlib import contextmanager
16
16
17 from IPython.core.getipython import get_ipython
17 from IPython.core.getipython import get_ipython
18 from IPython.kernel.comm import Comm
18 from IPython.kernel.comm import Comm
19 from IPython.config import LoggingConfigurable
19 from IPython.config import LoggingConfigurable
20 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int
20 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int
21 from IPython.utils.py3compat import string_types
21 from IPython.utils.py3compat import string_types
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Classes
24 # Classes
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 class CallbackDispatcher(LoggingConfigurable):
26 class CallbackDispatcher(LoggingConfigurable):
27 """A structure for registering and running callbacks"""
27 """A structure for registering and running callbacks"""
28 callbacks = List()
28 callbacks = List()
29
29
30 def __call__(self, *args, **kwargs):
30 def __call__(self, *args, **kwargs):
31 """Call all of the registered callbacks."""
31 """Call all of the registered callbacks."""
32 value = None
32 value = None
33 for callback in self.callbacks:
33 for callback in self.callbacks:
34 try:
34 try:
35 local_value = callback(*args, **kwargs)
35 local_value = callback(*args, **kwargs)
36 except Exception as e:
36 except Exception as e:
37 ip = get_ipython()
37 ip = get_ipython()
38 if ip is None:
38 if ip is None:
39 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
39 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
40 else:
40 else:
41 ip.showtraceback()
41 ip.showtraceback()
42 else:
42 else:
43 value = local_value if local_value is not None else value
43 value = local_value if local_value is not None else value
44 return value
44 return value
45
45
46 def register_callback(self, callback, remove=False):
46 def register_callback(self, callback, remove=False):
47 """(Un)Register a callback
47 """(Un)Register a callback
48
48
49 Parameters
49 Parameters
50 ----------
50 ----------
51 callback: method handle
51 callback: method handle
52 Method to be registered or unregistered.
52 Method to be registered or unregistered.
53 remove=False: bool
53 remove=False: bool
54 Whether to unregister the callback."""
54 Whether to unregister the callback."""
55
55
56 # (Un)Register the callback.
56 # (Un)Register the callback.
57 if remove and callback in self.callbacks:
57 if remove and callback in self.callbacks:
58 self.callbacks.remove(callback)
58 self.callbacks.remove(callback)
59 elif not remove and callback not in self.callbacks:
59 elif not remove and callback not in self.callbacks:
60 self.callbacks.append(callback)
60 self.callbacks.append(callback)
61
61
62 def _show_traceback(method):
62 def _show_traceback(method):
63 """decorator for showing tracebacks in IPython"""
63 """decorator for showing tracebacks in IPython"""
64 def m(self, *args, **kwargs):
64 def m(self, *args, **kwargs):
65 try:
65 try:
66 return(method(self, *args, **kwargs))
66 return(method(self, *args, **kwargs))
67 except Exception as e:
67 except Exception as e:
68 ip = get_ipython()
68 ip = get_ipython()
69 if ip is None:
69 if ip is None:
70 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
70 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
71 else:
71 else:
72 ip.showtraceback()
72 ip.showtraceback()
73 return m
73 return m
74
74
75 class Widget(LoggingConfigurable):
75 class Widget(LoggingConfigurable):
76 #-------------------------------------------------------------------------
76 #-------------------------------------------------------------------------
77 # Class attributes
77 # Class attributes
78 #-------------------------------------------------------------------------
78 #-------------------------------------------------------------------------
79 _widget_construction_callback = None
79 _widget_construction_callback = None
80 widgets = {}
80 widgets = {}
81
81
82 @staticmethod
82 @staticmethod
83 def on_widget_constructed(callback):
83 def on_widget_constructed(callback):
84 """Registers a callback to be called when a widget is constructed.
84 """Registers a callback to be called when a widget is constructed.
85
85
86 The callback must have the following signature:
86 The callback must have the following signature:
87 callback(widget)"""
87 callback(widget)"""
88 Widget._widget_construction_callback = callback
88 Widget._widget_construction_callback = callback
89
89
90 @staticmethod
90 @staticmethod
91 def _call_widget_constructed(widget):
91 def _call_widget_constructed(widget):
92 """Static method, called when a widget is constructed."""
92 """Static method, called when a widget is constructed."""
93 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
93 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
94 Widget._widget_construction_callback(widget)
94 Widget._widget_construction_callback(widget)
95
95
96 #-------------------------------------------------------------------------
96 #-------------------------------------------------------------------------
97 # Traits
97 # Traits
98 #-------------------------------------------------------------------------
98 #-------------------------------------------------------------------------
99 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
99 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
100 registered in the front-end to create and sync this widget with.""")
100 registered in the front-end to create and sync this widget with.""")
101 _view_name = Unicode(help="""Default view registered in the front-end
101 _view_name = Unicode(help="""Default view registered in the front-end
102 to use to represent the widget.""", sync=True)
102 to use to represent the widget.""", sync=True)
103 _comm = Instance('IPython.kernel.comm.Comm')
103 _comm = Instance('IPython.kernel.comm.Comm')
104
104
105 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
105 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
106 front-end can send before receiving an idle msg from the back-end.""")
106 front-end can send before receiving an idle msg from the back-end.""")
107
107
108 keys = List()
108 keys = List()
109 def _keys_default(self):
109 def _keys_default(self):
110 return [name for name in self.traits(sync=True)]
110 return [name for name in self.traits(sync=True)]
111
111
112 _property_lock = Tuple((None, None))
112 _property_lock = Tuple((None, None))
113
113
114 _display_callbacks = Instance(CallbackDispatcher, ())
114 _display_callbacks = Instance(CallbackDispatcher, ())
115 _msg_callbacks = Instance(CallbackDispatcher, ())
115 _msg_callbacks = Instance(CallbackDispatcher, ())
116
116
117 #-------------------------------------------------------------------------
117 #-------------------------------------------------------------------------
118 # (Con/de)structor
118 # (Con/de)structor
119 #-------------------------------------------------------------------------
119 #-------------------------------------------------------------------------
120 def __init__(self, **kwargs):
120 def __init__(self, **kwargs):
121 """Public constructor"""
121 """Public constructor"""
122 super(Widget, self).__init__(**kwargs)
122 super(Widget, self).__init__(**kwargs)
123
123
124 self.on_trait_change(self._handle_property_changed, self.keys)
124 self.on_trait_change(self._handle_property_changed, self.keys)
125 Widget._call_widget_constructed(self)
125 Widget._call_widget_constructed(self)
126
126
127 def __del__(self):
127 def __del__(self):
128 """Object disposal"""
128 """Object disposal"""
129 self.close()
129 self.close()
130
130
131 #-------------------------------------------------------------------------
131 #-------------------------------------------------------------------------
132 # Properties
132 # Properties
133 #-------------------------------------------------------------------------
133 #-------------------------------------------------------------------------
134
134
135 @property
135 @property
136 def comm(self):
136 def comm(self):
137 """Gets the Comm associated with this widget.
137 """Gets the Comm associated with this widget.
138
138
139 If a Comm doesn't exist yet, a Comm will be created automagically."""
139 If a Comm doesn't exist yet, a Comm will be created automagically."""
140 if self._comm is None:
140 if self._comm is None:
141 # Create a comm.
141 # Create a comm.
142 self._comm = Comm(target_name=self._model_name)
142 self._comm = Comm(target_name=self._model_name)
143 self._comm.on_msg(self._handle_msg)
143 self._comm.on_msg(self._handle_msg)
144 Widget.widgets[self.model_id] = self
144 Widget.widgets[self.model_id] = self
145
145
146 # first update
146 # first update
147 self.send_state()
147 self.send_state()
148 return self._comm
148 return self._comm
149
149
150 @property
150 @property
151 def model_id(self):
151 def model_id(self):
152 """Gets the model id of this widget.
152 """Gets the model id of this widget.
153
153
154 If a Comm doesn't exist yet, a Comm will be created automagically."""
154 If a Comm doesn't exist yet, a Comm will be created automagically."""
155 return self.comm.comm_id
155 return self.comm.comm_id
156
156
157 #-------------------------------------------------------------------------
157 #-------------------------------------------------------------------------
158 # Methods
158 # Methods
159 #-------------------------------------------------------------------------
159 #-------------------------------------------------------------------------
160
160
161 def close(self):
161 def close(self):
162 """Close method.
162 """Close method.
163
163
164 Closes the underlying comm.
164 Closes the underlying comm.
165 When the comm is closed, all of the widget views are automatically
165 When the comm is closed, all of the widget views are automatically
166 removed from the front-end."""
166 removed from the front-end."""
167 if self._comm is not None:
167 if self._comm is not None:
168 Widget.widgets.pop(self.model_id, None)
168 Widget.widgets.pop(self.model_id, None)
169 self._comm.close()
169 self._comm.close()
170 self._comm = None
170 self._comm = None
171
171
172 def send_state(self, key=None):
172 def send_state(self, key=None):
173 """Sends the widget state, or a piece of it, to the front-end.
173 """Sends the widget state, or a piece of it, to the front-end.
174
174
175 Parameters
175 Parameters
176 ----------
176 ----------
177 key : unicode (optional)
177 key : unicode (optional)
178 A single property's name to sync with the front-end.
178 A single property's name to sync with the front-end.
179 """
179 """
180 self._send({
180 self._send({
181 "method" : "update",
181 "method" : "update",
182 "state" : self.get_state()
182 "state" : self.get_state()
183 })
183 })
184
184
185 def get_state(self, key=None):
185 def get_state(self, key=None):
186 """Gets the widget state, or a piece of it.
186 """Gets the widget state, or a piece of it.
187
187
188 Parameters
188 Parameters
189 ----------
189 ----------
190 key : unicode (optional)
190 key : unicode (optional)
191 A single property's name to get.
191 A single property's name to get.
192 """
192 """
193 keys = self.keys if key is None else [key]
193 keys = self.keys if key is None else [key]
194 state = {}
194 state = {}
195 for k in keys:
195 for k in keys:
196 f = self.trait_metadata(k, 'to_json')
196 f = self.trait_metadata(k, 'to_json')
197 if f is None:
197 if f is None:
198 f = self._trait_to_json
198 f = self._trait_to_json
199 value = getattr(self, k)
199 value = getattr(self, k)
200 state[k] = f(value)
200 state[k] = f(value)
201 return state
201 return state
202
202
203 def send(self, content):
203 def send(self, content):
204 """Sends a custom msg to the widget model in the front-end.
204 """Sends a custom msg to the widget model in the front-end.
205
205
206 Parameters
206 Parameters
207 ----------
207 ----------
208 content : dict
208 content : dict
209 Content of the message to send.
209 Content of the message to send.
210 """
210 """
211 self._send({"method": "custom", "content": content})
211 self._send({"method": "custom", "content": content})
212
212
213 def on_msg(self, callback, remove=False):
213 def on_msg(self, callback, remove=False):
214 """(Un)Register a custom msg receive callback.
214 """(Un)Register a custom msg receive callback.
215
215
216 Parameters
216 Parameters
217 ----------
217 ----------
218 callback: callable
218 callback: callable
219 callback will be passed two arguments when a message arrives::
219 callback will be passed two arguments when a message arrives::
220
220
221 callback(widget, content)
221 callback(widget, content)
222
222
223 remove: bool
223 remove: bool
224 True if the callback should be unregistered."""
224 True if the callback should be unregistered."""
225 self._msg_callbacks.register_callback(callback, remove=remove)
225 self._msg_callbacks.register_callback(callback, remove=remove)
226
226
227 def on_displayed(self, callback, remove=False):
227 def on_displayed(self, callback, remove=False):
228 """(Un)Register a widget displayed callback.
228 """(Un)Register a widget displayed callback.
229
229
230 Parameters
230 Parameters
231 ----------
231 ----------
232 callback: method handler
232 callback: method handler
233 Must have a signature of::
233 Must have a signature of::
234
234
235 callback(widget, **kwargs)
235 callback(widget, **kwargs)
236
236
237 kwargs from display are passed through without modification.
237 kwargs from display are passed through without modification.
238 remove: bool
238 remove: bool
239 True if the callback should be unregistered."""
239 True if the callback should be unregistered."""
240 self._display_callbacks.register_callback(callback, remove=remove)
240 self._display_callbacks.register_callback(callback, remove=remove)
241
241
242 #-------------------------------------------------------------------------
242 #-------------------------------------------------------------------------
243 # Support methods
243 # Support methods
244 #-------------------------------------------------------------------------
244 #-------------------------------------------------------------------------
245 @contextmanager
245 @contextmanager
246 def _lock_property(self, key, value):
246 def _lock_property(self, key, value):
247 """Lock a property-value pair.
247 """Lock a property-value pair.
248
248
249 NOTE: This, in addition to the single lock for all state changes, is
249 NOTE: This, in addition to the single lock for all state changes, is
250 flawed. In the future we may want to look into buffering state changes
250 flawed. In the future we may want to look into buffering state changes
251 back to the front-end."""
251 back to the front-end."""
252 self._property_lock = (key, value)
252 self._property_lock = (key, value)
253 try:
253 try:
254 yield
254 yield
255 finally:
255 finally:
256 self._property_lock = (None, None)
256 self._property_lock = (None, None)
257
257
258 def _should_send_property(self, key, value):
258 def _should_send_property(self, key, value):
259 """Check the property lock (property_lock)"""
259 """Check the property lock (property_lock)"""
260 return key != self._property_lock[0] or \
260 return key != self._property_lock[0] or \
261 value != self._property_lock[1]
261 value != self._property_lock[1]
262
262
263 # Event handlers
263 # Event handlers
264 @_show_traceback
264 @_show_traceback
265 def _handle_msg(self, msg):
265 def _handle_msg(self, msg):
266 """Called when a msg is received from the front-end"""
266 """Called when a msg is received from the front-end"""
267 data = msg['content']['data']
267 data = msg['content']['data']
268 method = data['method']
268 method = data['method']
269 if not method in ['backbone', 'custom']:
269 if not method in ['backbone', 'custom']:
270 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
270 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
271
271
272 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
272 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
273 if method == 'backbone' and 'sync_data' in data:
273 if method == 'backbone' and 'sync_data' in data:
274 sync_data = data['sync_data']
274 sync_data = data['sync_data']
275 self._handle_receive_state(sync_data) # handles all methods
275 self._handle_receive_state(sync_data) # handles all methods
276
276
277 # Handle a custom msg from the front-end
277 # Handle a custom msg from the front-end
278 elif method == 'custom':
278 elif method == 'custom':
279 if 'content' in data:
279 if 'content' in data:
280 self._handle_custom_msg(data['content'])
280 self._handle_custom_msg(data['content'])
281
281
282 def _handle_receive_state(self, sync_data):
282 def _handle_receive_state(self, sync_data):
283 """Called when a state is received from the front-end."""
283 """Called when a state is received from the front-end."""
284 for name in self.keys:
284 for name in self.keys:
285 if name in sync_data:
285 if name in sync_data:
286 f = self.trait_metadata(name, 'from_json')
286 f = self.trait_metadata(name, 'from_json')
287 if f is None:
287 if f is None:
288 f = self._trait_from_json
288 f = self._trait_from_json
289 value = f(sync_data[name])
289 value = f(sync_data[name])
290 with self._lock_property(name, value):
290 with self._lock_property(name, value):
291 setattr(self, name, value)
291 setattr(self, name, value)
292
292
293 def _handle_custom_msg(self, content):
293 def _handle_custom_msg(self, content):
294 """Called when a custom msg is received."""
294 """Called when a custom msg is received."""
295 self._msg_callbacks(self, content)
295 self._msg_callbacks(self, content)
296
296
297 def _handle_property_changed(self, name, old, new):
297 def _handle_property_changed(self, name, old, new):
298 """Called when a property has been changed."""
298 """Called when a property has been changed."""
299 # Make sure this isn't information that the front-end just sent us.
299 # Make sure this isn't information that the front-end just sent us.
300 if self._should_send_property(name, new):
300 if self._should_send_property(name, new):
301 # Send new state to front-end
301 # Send new state to front-end
302 self.send_state(key=name)
302 self.send_state(key=name)
303
303
304 def _handle_displayed(self, **kwargs):
304 def _handle_displayed(self, **kwargs):
305 """Called when a view has been displayed for this widget instance"""
305 """Called when a view has been displayed for this widget instance"""
306 self._display_callbacks(self, **kwargs)
306 self._display_callbacks(self, **kwargs)
307
307
308 def _trait_to_json(self, x):
308 def _trait_to_json(self, x):
309 """Convert a trait value to json
309 """Convert a trait value to json
310
310
311 Traverse lists/tuples and dicts and serialize their values as well.
311 Traverse lists/tuples and dicts and serialize their values as well.
312 Replace any widgets with their model_id
312 Replace any widgets with their model_id
313 """
313 """
314 if isinstance(x, dict):
314 if isinstance(x, dict):
315 return {k: self._trait_to_json(v) for k, v in x.items()}
315 return {k: self._trait_to_json(v) for k, v in x.items()}
316 elif isinstance(x, (list, tuple)):
316 elif isinstance(x, (list, tuple)):
317 return [self._trait_to_json(v) for v in x]
317 return [self._trait_to_json(v) for v in x]
318 elif isinstance(x, Widget):
318 elif isinstance(x, Widget):
319 return "IPY_MODEL_" + x.model_id
319 return "IPY_MODEL_" + x.model_id
320 else:
320 else:
321 return x # Value must be JSON-able
321 return x # Value must be JSON-able
322
322
323 def _trait_from_json(self, x):
323 def _trait_from_json(self, x):
324 """Convert json values to objects
324 """Convert json values to objects
325
325
326 Replace any strings representing valid model id values to Widget references.
326 Replace any strings representing valid model id values to Widget references.
327 """
327 """
328 if isinstance(x, dict):
328 if isinstance(x, dict):
329 return {k: self._trait_from_json(v) for k, v in x.items()}
329 return {k: self._trait_from_json(v) for k, v in x.items()}
330 elif isinstance(x, (list, tuple)):
330 elif isinstance(x, (list, tuple)):
331 return [self._trait_from_json(v) for v in x]
331 return [self._trait_from_json(v) for v in x]
332 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
332 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
333 # we want to support having child widgets at any level in a hierarchy
333 # we want to support having child widgets at any level in a hierarchy
334 # trusting that a widget UUID will not appear out in the wild
334 # trusting that a widget UUID will not appear out in the wild
335 return Widget.widgets[x]
335 return Widget.widgets[x]
336 else:
336 else:
337 return x
337 return x
338
338
339 def _ipython_display_(self, **kwargs):
339 def _ipython_display_(self, **kwargs):
340 """Called when `IPython.display.display` is called on the widget."""
340 """Called when `IPython.display.display` is called on the widget."""
341 # Show view. By sending a display message, the comm is opened and the
341 # Show view. By sending a display message, the comm is opened and the
342 # initial state is sent.
342 # initial state is sent.
343 self._send({"method": "display"})
343 self._send({"method": "display"})
344 self._handle_displayed(**kwargs)
344 self._handle_displayed(**kwargs)
345
345
346 def _send(self, msg):
346 def _send(self, msg):
347 """Sends a message to the model in the front-end."""
347 """Sends a message to the model in the front-end."""
348 self.comm.send(msg)
348 self.comm.send(msg)
349
349
350
350
351 class DOMWidget(Widget):
351 class DOMWidget(Widget):
352 visible = Bool(True, help="Whether the widget is visible.", sync=True)
352 visible = Bool(True, help="Whether the widget is visible.", sync=True)
353 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
353 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
354
354
355 def get_css(self, key, selector=""):
355 def get_css(self, key, selector=""):
356 """Get a CSS property of the widget.
356 """Get a CSS property of the widget.
357
357
358 Note: This function does not actually request the CSS from the
358 Note: This function does not actually request the CSS from the
359 front-end; Only properties that have been set with set_css can be read.
359 front-end; Only properties that have been set with set_css can be read.
360
360
361 Parameters
361 Parameters
362 ----------
362 ----------
363 key: unicode
363 key: unicode
364 CSS key
364 CSS key
365 selector: unicode (optional)
365 selector: unicode (optional)
366 JQuery selector used when the CSS key/value was set.
366 JQuery selector used when the CSS key/value was set.
367 """
367 """
368 if selector in self._css and key in self._css[selector]:
368 if selector in self._css and key in self._css[selector]:
369 return self._css[selector][key]
369 return self._css[selector][key]
370 else:
370 else:
371 return None
371 return None
372
372
373 def set_css(self, dict_or_key, value=None, selector=''):
373 def set_css(self, dict_or_key, value=None, selector=''):
374 """Set one or more CSS properties of the widget.
374 """Set one or more CSS properties of the widget.
375
375
376 This function has two signatures:
376 This function has two signatures:
377 - set_css(css_dict, selector='')
377 - set_css(css_dict, selector='')
378 - set_css(key, value, selector='')
378 - set_css(key, value, selector='')
379
379
380 Parameters
380 Parameters
381 ----------
381 ----------
382 css_dict : dict
382 css_dict : dict
383 CSS key/value pairs to apply
383 CSS key/value pairs to apply
384 key: unicode
384 key: unicode
385 CSS key
385 CSS key
386 value:
386 value:
387 CSS value
387 CSS value
388 selector: unicode (optional, kwarg only)
388 selector: unicode (optional, kwarg only)
389 JQuery selector to use to apply the CSS key/value. If no selector
389 JQuery selector to use to apply the CSS key/value. If no selector
390 is provided, an empty selector is used. An empty selector makes the
390 is provided, an empty selector is used. An empty selector makes the
391 front-end try to apply the css to a default element. The default
391 front-end try to apply the css to the top-level element.
392 element is an attribute unique to each view, which is a DOM element
393 of the view that should be styled with common CSS (see
394 `$el_to_style` in the Javascript code).
395 """
392 """
396 if value is None:
393 if value is None:
397 css_dict = dict_or_key
394 css_dict = dict_or_key
398 else:
395 else:
399 css_dict = {dict_or_key: value}
396 css_dict = {dict_or_key: value}
400
397
401 for (key, value) in css_dict.items():
398 for (key, value) in css_dict.items():
402 # First remove the selector/key pair from the css list if it exists.
399 # First remove the selector/key pair from the css list if it exists.
403 # Then add the selector/key pair and new value to the bottom of the
400 # Then add the selector/key pair and new value to the bottom of the
404 # list.
401 # list.
405 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
402 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
406 self._css += [(selector, key, value)]
403 self._css += [(selector, key, value)]
407 self.send_state('_css')
404 self.send_state('_css')
408
405
409 def add_class(self, class_names, selector=""):
406 def add_class(self, class_names, selector=""):
410 """Add class[es] to a DOM element.
407 """Add class[es] to a DOM element.
411
408
412 Parameters
409 Parameters
413 ----------
410 ----------
414 class_names: unicode or list
411 class_names: unicode or list
415 Class name(s) to add to the DOM element(s).
412 Class name(s) to add to the DOM element(s).
416 selector: unicode (optional)
413 selector: unicode (optional)
417 JQuery selector to select the DOM element(s) that the class(es) will
414 JQuery selector to select the DOM element(s) that the class(es) will
418 be added to.
415 be added to.
419 """
416 """
420 class_list = class_names
417 class_list = class_names
421 if isinstance(class_list, (list, tuple)):
418 if isinstance(class_list, (list, tuple)):
422 class_list = ' '.join(class_list)
419 class_list = ' '.join(class_list)
423
420
424 self.send({
421 self.send({
425 "msg_type" : "add_class",
422 "msg_type" : "add_class",
426 "class_list" : class_list,
423 "class_list" : class_list,
427 "selector" : selector
424 "selector" : selector
428 })
425 })
429
426
430 def remove_class(self, class_names, selector=""):
427 def remove_class(self, class_names, selector=""):
431 """Remove class[es] from a DOM element.
428 """Remove class[es] from a DOM element.
432
429
433 Parameters
430 Parameters
434 ----------
431 ----------
435 class_names: unicode or list
432 class_names: unicode or list
436 Class name(s) to remove from the DOM element(s).
433 Class name(s) to remove from the DOM element(s).
437 selector: unicode (optional)
434 selector: unicode (optional)
438 JQuery selector to select the DOM element(s) that the class(es) will
435 JQuery selector to select the DOM element(s) that the class(es) will
439 be removed from.
436 be removed from.
440 """
437 """
441 class_list = class_names
438 class_list = class_names
442 if isinstance(class_list, (list, tuple)):
439 if isinstance(class_list, (list, tuple)):
443 class_list = ' '.join(class_list)
440 class_list = ' '.join(class_list)
444
441
445 self.send({
442 self.send({
446 "msg_type" : "remove_class",
443 "msg_type" : "remove_class",
447 "class_list" : class_list,
444 "class_list" : class_list,
448 "selector" : selector,
445 "selector" : selector,
449 })
446 })
General Comments 0
You need to be logged in to leave comments. Login now