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