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