widget.js
817 lines
| 32.3 KiB
| application/javascript
|
JavascriptLexer
Jonathan Frederic
|
r17198 | // Copyright (c) IPython Development Team. | ||
// Distributed under the terms of the Modified BSD License. | ||||
Jonathan Frederic
|
r14546 | |||
Jonathan Frederic
|
r15427 | define(["widgets/js/manager", | ||
Jonathan Frederic
|
r14546 | "underscore", | ||
Jason Grout
|
r18892 | "backbone", | ||
"jquery", | ||||
"base/js/utils", | ||||
Jonathan Frederic
|
r17216 | "base/js/namespace", | ||
Jonathan Frederic
|
r18907 | ], function(widgetmanager, _, Backbone, $, utils, IPython){ | ||
Matthias Bussonnier
|
r19739 | "use strict"; | ||
Jonathan Frederic
|
r14609 | |||
Jonathan Frederic
|
r14546 | var WidgetModel = Backbone.Model.extend({ | ||
Jason Grout
|
r18888 | constructor: function (widget_manager, model_id, comm) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Constructor | ||||
* | ||||
* Creates a WidgetModel instance. | ||||
* | ||||
* Parameters | ||||
* ---------- | ||||
* widget_manager : WidgetManager instance | ||||
* model_id : string | ||||
* An ID unique to this model. | ||||
* comm : Comm instance (optional) | ||||
*/ | ||||
Jonathan Frederic
|
r14546 | this.widget_manager = widget_manager; | ||
Jonathan Frederic
|
r18907 | this.state_change = Promise.resolve(); | ||
Jonathan Frederic
|
r15280 | this._buffered_state_diff = {}; | ||
Jonathan Frederic
|
r14546 | this.pending_msgs = 0; | ||
this.msg_buffer = null; | ||||
sylvain.corlay
|
r17839 | this.state_lock = null; | ||
Jonathan Frederic
|
r14546 | this.id = model_id; | ||
Sylvain Corlay
|
r18007 | this.views = {}; | ||
Jason Grout
|
r20839 | this.serializers = {}; | ||
Jonathan Frederic
|
r19360 | this._resolve_received_state = {}; | ||
Jonathan Frederic
|
r14546 | |||
if (comm !== undefined) { | ||||
// Remember comm associated with the model. | ||||
this.comm = comm; | ||||
comm.model = this; | ||||
// Hook comm messages up to model. | ||||
comm.on_close($.proxy(this._handle_comm_closed, this)); | ||||
comm.on_msg($.proxy(this._handle_comm_msg, this)); | ||||
Jonathan Frederic
|
r19350 | |||
// Assume the comm is alive. | ||||
Jonathan Frederic
|
r19360 | this.set_comm_live(true); | ||
Jonathan Frederic
|
r19350 | } else { | ||
Jonathan Frederic
|
r19360 | this.set_comm_live(false); | ||
Jonathan Frederic
|
r14546 | } | ||
Jonathan Frederic
|
r19566 | |||
// Listen for the events that lead to the websocket being terminated. | ||||
var that = this; | ||||
var died = function() { | ||||
that.set_comm_live(false); | ||||
}; | ||||
widget_manager.notebook.events.on('kernel_disconnected.Kernel', died); | ||||
widget_manager.notebook.events.on('kernel_killed.Kernel', died); | ||||
widget_manager.notebook.events.on('kernel_restarting.Kernel', died); | ||||
widget_manager.notebook.events.on('kernel_dead.Kernel', died); | ||||
Jonathan Frederic
|
r14546 | return Backbone.Model.apply(this); | ||
}, | ||||
Jason Grout
|
r20839 | send: function (content, callbacks, buffers) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Send a custom msg over the comm. | ||||
*/ | ||||
Jonathan Frederic
|
r14546 | if (this.comm !== undefined) { | ||
Jonathan Frederic
|
r14655 | var data = {method: 'custom', content: content}; | ||
Jason Grout
|
r20839 | this.comm.send(data, callbacks, {}, buffers); | ||
Jonathan Frederic
|
r14741 | this.pending_msgs++; | ||
Jonathan Frederic
|
r14546 | } | ||
}, | ||||
Jonathan Frederic
|
r19350 | request_state: function(callbacks) { | ||
/** | ||||
* Request a state push from the back-end. | ||||
*/ | ||||
if (!this.comm) { | ||||
console.error("Could not request_state because comm doesn't exist!"); | ||||
return; | ||||
} | ||||
Jonathan Frederic
|
r19351 | |||
Jonathan Frederic
|
r19360 | var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks()); | ||
Jonathan Frederic
|
r19351 | // Promise that is resolved when a state is received | ||
// from the back-end. | ||||
var that = this; | ||||
var received_state = new Promise(function(resolve) { | ||||
Jonathan Frederic
|
r19360 | that._resolve_received_state[msg_id] = resolve; | ||
Jonathan Frederic
|
r19351 | }); | ||
return received_state; | ||||
Jonathan Frederic
|
r19350 | }, | ||
Jonathan Frederic
|
r19360 | set_comm_live: function(live) { | ||
Jonathan Frederic
|
r19350 | /** | ||
Jonathan Frederic
|
r19360 | * Change the comm_live state of the model. | ||
Jonathan Frederic
|
r19350 | */ | ||
Jonathan Frederic
|
r19360 | if (this.comm_live === undefined || this.comm_live != live) { | ||
this.comm_live = live; | ||||
this.trigger(live ? 'comm:live' : 'comm:dead', {model: this}); | ||||
Jonathan Frederic
|
r19350 | } | ||
}, | ||||
close: function(comm_closed) { | ||||
Jonathan Frederic
|
r19176 | /** | ||
Jonathan Frederic
|
r19350 | * Close model | ||
Jonathan Frederic
|
r19176 | */ | ||
Jonathan Frederic
|
r19350 | if (this.comm && !comm_closed) { | ||
this.comm.close(); | ||||
} | ||||
Sylvain Corlay
|
r17857 | this.stopListening(); | ||
Sylvain Corlay
|
r17848 | this.trigger('destroy', this); | ||
Jonathan Frederic
|
r14546 | delete this.comm.model; // Delete ref so GC will collect widget model. | ||
delete this.comm; | ||||
delete this.model_id; // Delete id from model so widget manager cleans up. | ||||
Jason Grout
|
r19170 | _.each(this.views, function(v, id, views) { | ||
v.then(function(view) { | ||||
view.remove(); | ||||
delete views[id]; | ||||
}); | ||||
}); | ||||
Jonathan Frederic
|
r14546 | }, | ||
Jonathan Frederic
|
r19350 | _handle_comm_closed: function (msg) { | ||
/** | ||||
* Handle when a widget is closed. | ||||
*/ | ||||
this.trigger('comm:close'); | ||||
this.close(true); | ||||
}, | ||||
Jonathan Frederic
|
r14546 | _handle_comm_msg: function (msg) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Handle incoming comm msg. | ||||
*/ | ||||
Jonathan Frederic
|
r14546 | var method = msg.content.data.method; | ||
Jason Grout
|
r20839 | |||
Jason Grout
|
r18893 | var that = this; | ||
Jonathan Frederic
|
r14546 | switch (method) { | ||
case 'update': | ||||
Jonathan Frederic
|
r19360 | this.state_change = this.state_change | ||
.then(function() { | ||||
Jason Grout
|
r20839 | var state = msg.content.data.state || {}; | ||
var buffer_keys = msg.content.data.buffers || []; | ||||
var buffers = msg.buffers || []; | ||||
var metadata = msg.content.data.metadata || {}; | ||||
var i,k; | ||||
for (var i=0; i<buffer_keys.length; i++) { | ||||
k = buffer_keys[i]; | ||||
state[k] = buffers[i]; | ||||
} | ||||
// for any metadata specifying a deserializer, set the | ||||
// state to a promise that resolves to the deserialized version | ||||
// also, store the serialization function for the attribute | ||||
var keys = Object.keys(metadata); | ||||
for (var i=0; i<keys.length; i++) { | ||||
k = keys[i]; | ||||
if (metadata[k] && metadata[k].serialization) { | ||||
that.serializers[k] = utils.load_class.apply(that, | ||||
metadata[k].serialization); | ||||
state[k] = that.deserialize(that.serializers[k], state[k]); | ||||
} | ||||
} | ||||
return utils.resolve_promises_dict(state); | ||||
}).then(function(state) { | ||||
return that.set_state(state); | ||||
Jonathan Frederic
|
r19360 | }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true)) | ||
.then(function() { | ||||
var parent_id = msg.parent_header.msg_id; | ||||
if (that._resolve_received_state[parent_id] !== undefined) { | ||||
that._resolve_received_state[parent_id].call(); | ||||
delete that._resolve_received_state[parent_id]; | ||||
} | ||||
Jonathan Frederic
|
r19361 | }).catch(utils.reject("Couldn't resolve state request promise", true)); | ||
Jonathan Frederic
|
r14546 | break; | ||
case 'custom': | ||||
Jason Grout
|
r20839 | this.trigger('msg:custom', msg.content.data.content, msg.buffers); | ||
Jonathan Frederic
|
r14546 | break; | ||
Jonathan Frederic
|
r14559 | case 'display': | ||
Jason Grout
|
r20452 | this.state_change = this.state_change.then(function() { | ||
that.widget_manager.display_view(msg, that); | ||||
}).catch(utils.reject('Could not process display view msg', true)); | ||||
Jonathan Frederic
|
r14559 | break; | ||
Jonathan Frederic
|
r14546 | } | ||
}, | ||||
Jason Grout
|
r20839 | deserialize: function(serializer, value) { | ||
// given a serializer dict and a value, | ||||
// return a promise for the deserialized value | ||||
var that = this; | ||||
return serializer.then(function(s) { | ||||
if (s.deserialize) { | ||||
return s.deserialize(value, that); | ||||
} else { | ||||
return value; | ||||
} | ||||
}); | ||||
}, | ||||
Jonathan Frederic
|
r18072 | set_state: function (state) { | ||
Jason Grout
|
r18888 | var that = this; | ||
Jonathan Frederic
|
r14609 | // Handle when a widget is updated via the python side. | ||
Jason Grout
|
r20839 | return new Promise(function(resolve, reject) { | ||
Jason Grout
|
r18888 | that.state_lock = state; | ||
try { | ||||
Jason Grout
|
r18893 | WidgetModel.__super__.set.call(that, state); | ||
Jason Grout
|
r18888 | } finally { | ||
that.state_lock = null; | ||||
Jonathan Frederic
|
r18884 | } | ||
Jason Grout
|
r20839 | resolve(); | ||
Jonathan Frederic
|
r19361 | }).catch(utils.reject("Couldn't set model state", true)); | ||
Jonathan Frederic
|
r19350 | }, | ||
get_state: function() { | ||||
// Get the serializable state of the model. | ||||
Jason Grout
|
r20839 | // Equivalent to Backbone.Model.toJSON() | ||
return _.clone(this.attributes); | ||||
Jonathan Frederic
|
r14546 | }, | ||
Jason Grout
|
r20839 | |||
Jonathan Frederic
|
r14546 | _handle_status: function (msg, callbacks) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Handle status msgs. | ||||
* | ||||
* execution_state : ('busy', 'idle', 'starting') | ||||
*/ | ||||
Jonathan Frederic
|
r14596 | if (this.comm !== undefined) { | ||
if (msg.content.execution_state ==='idle') { | ||||
// Send buffer if this message caused another message to be | ||||
// throttled. | ||||
if (this.msg_buffer !== null && | ||||
Jonathan Frederic
|
r15369 | (this.get('msg_throttle') || 3) === this.pending_msgs) { | ||
Jonathan Frederic
|
r14596 | var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer}; | ||
MinRK
|
r14792 | this.comm.send(data, callbacks); | ||
Jonathan Frederic
|
r14596 | this.msg_buffer = null; | ||
} else { | ||||
--this.pending_msgs; | ||||
} | ||||
Jonathan Frederic
|
r14546 | } | ||
} | ||||
}, | ||||
Jonathan Frederic
|
r14640 | callbacks: function(view) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Create msg callbacks for a comm msg. | ||||
*/ | ||||
Jonathan Frederic
|
r14640 | var callbacks = this.widget_manager.callbacks(view); | ||
if (callbacks.iopub === undefined) { | ||||
callbacks.iopub = {}; | ||||
} | ||||
Jason Grout
|
r14639 | var that = this; | ||
callbacks.iopub.status = function (msg) { | ||||
that._handle_status(msg, callbacks); | ||||
MinRK
|
r14792 | }; | ||
Jason Grout
|
r14639 | return callbacks; | ||
}, | ||||
Jonathan Frederic
|
r14546 | |||
Jonathan Frederic
|
r15279 | set: function(key, val, options) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Set a value. | ||||
*/ | ||||
Jonathan Frederic
|
r15280 | var return_value = WidgetModel.__super__.set.apply(this, arguments); | ||
// Backbone only remembers the diff of the most recent set() | ||||
Jonathan Frederic
|
r15281 | // operation. Calling set multiple times in a row results in a | ||
Jonathan Frederic
|
r15280 | // loss of diff information. Here we keep our own running diff. | ||
this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {}); | ||||
return return_value; | ||||
Jonathan Frederic
|
r15279 | }, | ||
Jason Grout
|
r14639 | sync: function (method, model, options) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Handle sync to the back-end. Called when a model.save() is called. | ||||
* | ||||
* Make sure a comm exists. | ||||
Jason Grout
|
r20839 | |||
* Parameters | ||||
* ---------- | ||||
* method : create, update, patch, delete, read | ||||
* create/update always send the full attribute set | ||||
* patch - only send attributes listed in options.attrs, and if we are queuing | ||||
* up messages, combine with previous messages that have not been sent yet | ||||
* model : the model we are syncing | ||||
* will normally be the same as `this` | ||||
* options : dict | ||||
* the `attrs` key, if it exists, gives an {attr: value} dict that should be synced, | ||||
* otherwise, sync all attributes | ||||
* | ||||
Jonathan Frederic
|
r19176 | */ | ||
Jonathan Frederic
|
r14640 | var error = options.error || function() { | ||
console.error('Backbone sync error:', arguments); | ||||
MinRK
|
r14792 | }; | ||
Jason Grout
|
r14639 | if (this.comm === undefined) { | ||
error(); | ||||
return false; | ||||
} | ||||
Jason Grout
|
r20839 | var attrs = (method === 'patch') ? options.attrs : model.get_state(options); | ||
// the state_lock lists attributes that are currently be changed right now from a kernel message | ||||
// we don't want to send these non-changes back to the kernel, so we delete them out of attrs | ||||
// (but we only delete them if the value hasn't changed from the value stored in the state_lock | ||||
sylvain.corlay
|
r17839 | if (this.state_lock !== null) { | ||
var keys = Object.keys(this.state_lock); | ||||
Sylvain Corlay
|
r17925 | for (var i=0; i<keys.length; i++) { | ||
sylvain.corlay
|
r17839 | var key = keys[i]; | ||
if (attrs[key] === this.state_lock[key]) { | ||||
delete attrs[key]; | ||||
} | ||||
Jason Grout
|
r14639 | } | ||
} | ||||
Jason Grout
|
r20839 | |||
Jonathan Frederic
|
r14686 | if (_.size(attrs) > 0) { | ||
Jonathan Frederic
|
r14741 | |||
// If this message was sent via backbone itself, it will not | ||||
// have any callbacks. It's important that we create callbacks | ||||
// so we can listen for status messages, etc... | ||||
var callbacks = options.callbacks || this.callbacks(); | ||||
// Check throttle. | ||||
Jonathan Frederic
|
r15369 | if (this.pending_msgs >= (this.get('msg_throttle') || 3)) { | ||
Jonathan Frederic
|
r14640 | // The throttle has been exceeded, buffer the current msg so | ||
// it can be sent once the kernel has finished processing | ||||
// some of the existing messages. | ||||
// Combine updates if it is a 'patch' sync, otherwise replace updates | ||||
switch (method) { | ||||
case 'patch': | ||||
Jonathan Frederic
|
r14662 | this.msg_buffer = $.extend(this.msg_buffer || {}, attrs); | ||
Jonathan Frederic
|
r14640 | break; | ||
case 'update': | ||||
Jonathan Frederic
|
r14661 | case 'create': | ||
Jonathan Frederic
|
r14640 | this.msg_buffer = attrs; | ||
break; | ||||
default: | ||||
error(); | ||||
return false; | ||||
} | ||||
this.msg_buffer_callbacks = callbacks; | ||||
} else { | ||||
// We haven't exceeded the throttle, send the message like | ||||
Jonathan Frederic
|
r14741 | // normal. | ||
Jason Grout
|
r20839 | this.send_sync_message(attrs, callbacks); | ||
Jason Grout
|
r20852 | this.pending_msgs++; | ||
Jonathan Frederic
|
r14640 | } | ||
Jonathan Frederic
|
r14546 | } | ||
// Since the comm is a one-way communication, assume the message | ||||
Jason Grout
|
r14639 | // arrived. Don't call success since we don't have a model back from the server | ||
// this means we miss out on the 'sync' event. | ||||
Jonathan Frederic
|
r15280 | this._buffered_state_diff = {}; | ||
Jonathan Frederic
|
r14546 | }, | ||
Jason Grout
|
r14618 | |||
Jason Grout
|
r20839 | send_sync_message: function(attrs, callbacks) { | ||
// prepare and send a comm message syncing attrs | ||||
Jonathan Frederic
|
r17199 | var that = this; | ||
Jason Grout
|
r20839 | // first, build a state dictionary with key=the attribute and the value | ||
// being the value or the promise of the serialized value | ||||
var state_promise_dict = {}; | ||||
var keys = Object.keys(attrs); | ||||
for (var i=0; i<keys.length; i++) { | ||||
// bind k and v locally; needed since we have an inner async function using v | ||||
(function(k,v) { | ||||
if (that.serializers[k]) { | ||||
state_promise_dict[k] = that.serializers[k].then(function(f) { | ||||
if (f.serialize) { | ||||
return f.serialize(v, that); | ||||
} else { | ||||
return v; | ||||
} | ||||
}) | ||||
} else { | ||||
state_promise_dict[k] = v; | ||||
} | ||||
})(keys[i], attrs[keys[i]]) | ||||
} | ||||
utils.resolve_promises_dict(state_promise_dict).then(function(state) { | ||||
// get binary values, then send | ||||
var keys = Object.keys(state); | ||||
var buffers = []; | ||||
var buffer_keys = []; | ||||
for (var i=0; i<keys.length; i++) { | ||||
var key = keys[i]; | ||||
var value = state[key]; | ||||
if (value.buffer instanceof ArrayBuffer | ||||
|| value instanceof ArrayBuffer) { | ||||
buffers.push(value); | ||||
buffer_keys.push(key); | ||||
delete state[key]; | ||||
} | ||||
} | ||||
that.comm.send({method: 'backbone', sync_data: state, buffer_keys: buffer_keys}, callbacks, {}, buffers); | ||||
Jason Grout
|
r20855 | }).catch(utils.reject("Couldn't send widget sync message", true)) | ||
Jason Grout
|
r20852 | .catch(function(error) { | ||
that.pending_msgs--; | ||||
return error; | ||||
}); | ||||
Jason Grout
|
r20839 | }, | ||
serialize: function(model, attrs) { | ||||
// Serialize the attributes into a sync message | ||||
var keys = Object.keys(attrs); | ||||
var key, value; | ||||
var buffers, metadata, buffer_keys, serialize; | ||||
for (var i=0; i<keys.length; i++) { | ||||
key = keys[i]; | ||||
serialize = model.serializers[key]; | ||||
if (serialize && serialize.serialize) { | ||||
attrs[key] = serialize.serialize(attrs[key]); | ||||
} | ||||
Jonathan Frederic
|
r14583 | } | ||
}, | ||||
Jason Grout
|
r20839 | save_changes: function(callbacks) { | ||
Jonathan Frederic
|
r19176 | /** | ||
Jason Grout
|
r20839 | * Push this model's state to the back-end | ||
* | ||||
* This invokes a Backbone.Sync. | ||||
Jonathan Frederic
|
r19176 | */ | ||
Jason Grout
|
r20839 | this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks}); | ||
Jonathan Frederic
|
r14583 | }, | ||
Sylvain Corlay
|
r18008 | on_some_change: function(keys, callback, context) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* on_some_change(["key1", "key2"], foo, context) differs from | ||||
* on("change:key1 change:key2", foo, context). | ||||
* If the widget attributes key1 and key2 are both modified, | ||||
* the second form will result in foo being called twice | ||||
* while the first will call foo only once. | ||||
*/ | ||||
sylvain.corlay
|
r17838 | this.on('change', function() { | ||
if (keys.some(this.hasChanged, this)) { | ||||
callback.apply(context); | ||||
} | ||||
}, this); | ||||
Jason Grout
|
r20839 | }, | ||
Jonathan Frederic
|
r14546 | }); | ||
Jonathan Frederic
|
r17202 | widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel); | ||
Jonathan Frederic
|
r14546 | |||
Jonathan Frederic
|
r14564 | var WidgetView = Backbone.View.extend({ | ||
Jonathan Frederic
|
r14565 | initialize: function(parameters) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Public constructor. | ||||
*/ | ||||
Jonathan Frederic
|
r14546 | this.model.on('change',this.update,this); | ||
Jonathan Frederic
|
r19572 | |||
// Bubble the comm live events. | ||||
Jonathan Frederic
|
r19566 | this.model.on('comm:live', function() { | ||
Jonathan Frederic
|
r19572 | this.trigger('comm:live', this); | ||
Jonathan Frederic
|
r19566 | }, this); | ||
this.model.on('comm:dead', function() { | ||||
Jonathan Frederic
|
r19572 | this.trigger('comm:dead', this); | ||
Jonathan Frederic
|
r19566 | }, this); | ||
Jonathan Frederic
|
r14565 | this.options = parameters.options; | ||
Sylvain Corlay
|
r17270 | this.on('displayed', function() { | ||
this.is_displayed = true; | ||||
}, this); | ||||
Jonathan Frederic
|
r14546 | }, | ||
update: function(){ | ||||
Jonathan Frederic
|
r19176 | /** | ||
* Triggered on model change. | ||||
* | ||||
* Update view to be consistent with this.model | ||||
*/ | ||||
Jonathan Frederic
|
r14546 | }, | ||
Jonathan Frederic
|
r14598 | create_child_view: function(child_model, options) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Create and promise that resolves to a child view of a given model | ||||
*/ | ||||
Jonathan Frederic
|
r18884 | var that = this; | ||
Jason Grout
|
r18888 | options = $.extend({ parent: this }, options || {}); | ||
Jason Grout
|
r20855 | return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view", true)); | ||
Jonathan Frederic
|
r14546 | }, | ||
callbacks: function(){ | ||||
Jonathan Frederic
|
r19176 | /** | ||
* Create msg callbacks for a comm msg. | ||||
*/ | ||||
Jonathan Frederic
|
r14640 | return this.model.callbacks(this); | ||
Jonathan Frederic
|
r14560 | }, | ||
Jonathan Frederic
|
r14546 | |||
render: function(){ | ||||
Jonathan Frederic
|
r19176 | /** | ||
* Render the view. | ||||
* | ||||
* By default, this is only called the first time the view is created | ||||
*/ | ||||
Jonathan Frederic
|
r14546 | }, | ||
Jonathan Frederic
|
r14609 | |||
Jason Grout
|
r20839 | send: function (content, buffers) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Send a custom msg associated with this view. | ||||
*/ | ||||
Jason Grout
|
r20839 | this.model.send(content, this.callbacks(), buffers); | ||
Jonathan Frederic
|
r14546 | }, | ||
touch: function () { | ||||
Jason Grout
|
r14639 | this.model.save_changes(this.callbacks()); | ||
Jonathan Frederic
|
r14546 | }, | ||
Sylvain Corlay
|
r17270 | |||
Sylvain Corlay
|
r17329 | after_displayed: function (callback, context) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Calls the callback right away is the view is already displayed | ||||
* otherwise, register the callback to the 'displayed' event. | ||||
*/ | ||||
Sylvain Corlay
|
r17270 | if (this.is_displayed) { | ||
callback.apply(context); | ||||
} else { | ||||
this.on('displayed', callback, context); | ||||
} | ||||
Jonathan Frederic
|
r19350 | }, | ||
remove: function () { | ||||
// Raise a remove event when the view is removed. | ||||
WidgetView.__super__.remove.apply(this, arguments); | ||||
this.trigger('remove'); | ||||
Nicholas Bollweg (Nick)
|
r19194 | } | ||
Jonathan Frederic
|
r14546 | }); | ||
Jonathan Frederic
|
r14609 | |||
Jonathan Frederic
|
r14564 | var DOMWidgetView = WidgetView.extend({ | ||
Sylvain Corlay
|
r17448 | initialize: function (parameters) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Public constructor | ||||
*/ | ||||
Sylvain Corlay
|
r17448 | DOMWidgetView.__super__.initialize.apply(this, [parameters]); | ||
sylvain.corlay
|
r17347 | this.model.on('change:visible', this.update_visible, this); | ||
this.model.on('change:_css', this.update_css, this); | ||||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17720 | this.model.on('change:_dom_classes', function(model, new_classes) { | ||
Jonathan Frederic
|
r17731 | var old_classes = model.previous('_dom_classes'); | ||
Jonathan Frederic
|
r17720 | this.update_classes(old_classes, new_classes); | ||
}, this); | ||||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17860 | this.model.on('change:color', function (model, value) { | ||
Jonathan Frederic
|
r17722 | this.update_attr('color', value); }, this); | ||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17860 | this.model.on('change:background_color', function (model, value) { | ||
Jonathan Frederic
|
r17722 | this.update_attr('background', value); }, this); | ||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17722 | this.model.on('change:width', function (model, value) { | ||
this.update_attr('width', value); }, this); | ||||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17722 | this.model.on('change:height', function (model, value) { | ||
this.update_attr('height', value); }, this); | ||||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17722 | this.model.on('change:border_color', function (model, value) { | ||
this.update_attr('border-color', value); }, this); | ||||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17722 | this.model.on('change:border_width', function (model, value) { | ||
this.update_attr('border-width', value); }, this); | ||||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17722 | this.model.on('change:border_style', function (model, value) { | ||
this.update_attr('border-style', value); }, this); | ||||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17722 | this.model.on('change:font_style', function (model, value) { | ||
this.update_attr('font-style', value); }, this); | ||||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17722 | this.model.on('change:font_weight', function (model, value) { | ||
this.update_attr('font-weight', value); }, this); | ||||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17722 | this.model.on('change:font_size', function (model, value) { | ||
Jonathan Frederic
|
r17947 | this.update_attr('font-size', this._default_px(value)); }, this); | ||
Jonathan Frederic
|
r17727 | |||
Jonathan Frederic
|
r17722 | this.model.on('change:font_family', function (model, value) { | ||
this.update_attr('font-family', value); }, this); | ||||
Jonathan Frederic
|
r17727 | |||
this.model.on('change:padding', function (model, value) { | ||||
this.update_attr('padding', value); }, this); | ||||
this.model.on('change:margin', function (model, value) { | ||||
Jonathan Frederic
|
r17947 | this.update_attr('margin', this._default_px(value)); }, this); | ||
this.model.on('change:border_radius', function (model, value) { | ||||
this.update_attr('border-radius', this._default_px(value)); }, this); | ||||
Jonathan Frederic
|
r17727 | |||
Sylvain Corlay
|
r17386 | this.after_displayed(function() { | ||
this.update_visible(this.model, this.model.get("visible")); | ||||
Jonathan Frederic
|
r17727 | this.update_classes([], this.model.get('_dom_classes')); | ||
Jonathan Frederic
|
r18177 | |||
Jonathan Frederic
|
r17860 | this.update_attr('color', this.model.get('color')); | ||
this.update_attr('background', this.model.get('background_color')); | ||||
Jonathan Frederic
|
r17727 | this.update_attr('width', this.model.get('width')); | ||
this.update_attr('height', this.model.get('height')); | ||||
this.update_attr('border-color', this.model.get('border_color')); | ||||
this.update_attr('border-width', this.model.get('border_width')); | ||||
this.update_attr('border-style', this.model.get('border_style')); | ||||
this.update_attr('font-style', this.model.get('font_style')); | ||||
this.update_attr('font-weight', this.model.get('font_weight')); | ||||
Jonathan Frederic
|
r19765 | this.update_attr('font-size', this._default_px(this.model.get('font_size'))); | ||
Jonathan Frederic
|
r17727 | this.update_attr('font-family', this.model.get('font_family')); | ||
this.update_attr('padding', this.model.get('padding')); | ||||
Jonathan Frederic
|
r19765 | this.update_attr('margin', this._default_px(this.model.get('margin'))); | ||
this.update_attr('border-radius', this._default_px(this.model.get('border_radius'))); | ||||
Jonathan Frederic
|
r18177 | |||
this.update_css(this.model, this.model.get("_css")); | ||||
Sylvain Corlay
|
r17386 | }, this); | ||
Jonathan Frederic
|
r14546 | }, | ||
Sylvain Corlay
|
r17386 | |||
Jonathan Frederic
|
r17947 | _default_px: function(value) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Makes browser interpret a numerical string as a pixel value. | ||||
*/ | ||||
Jonathan Frederic
|
r17947 | if (/^\d+\.?(\d+)?$/.test(value.trim())) { | ||
return value.trim() + 'px'; | ||||
Jonathan Frederic
|
r14546 | } | ||
Jonathan Frederic
|
r17947 | return value; | ||
Jonathan Frederic
|
r14546 | }, | ||
Jonathan Frederic
|
r17722 | update_attr: function(name, value) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Set a css attr of the widget view. | ||||
*/ | ||||
Jonathan Frederic
|
r17722 | this.$el.css(name, value); | ||
Jonathan Frederic
|
r14546 | }, | ||
Sylvain Corlay
|
r17386 | |||
sylvain.corlay
|
r17347 | update_visible: function(model, value) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Update visibility | ||||
*/ | ||||
Jason Grout
|
r19186 | switch(value) { | ||
case null: // python None | ||||
this.$el.show().css('visibility', 'hidden'); break; | ||||
case false: | ||||
this.$el.hide(); break; | ||||
case true: | ||||
this.$el.show().css('visibility', ''); break; | ||||
} | ||||
sylvain.corlay
|
r17347 | }, | ||
update_css: function (model, css) { | ||||
Jonathan Frederic
|
r19176 | /** | ||
* Update the css styling of this view. | ||||
*/ | ||||
Jonathan Frederic
|
r14546 | if (css === undefined) {return;} | ||
Jonathan Frederic
|
r17177 | for (var i = 0; i < css.length; i++) { | ||
Jonathan Frederic
|
r14664 | // Apply the css traits to all elements that match the selector. | ||
Jonathan Frederic
|
r17177 | var selector = css[i][0]; | ||
var elements = this._get_selector_element(selector); | ||||
Jonathan Frederic
|
r14664 | if (elements.length > 0) { | ||
Jonathan Frederic
|
r17177 | var trait_key = css[i][1]; | ||
var trait_value = css[i][2]; | ||||
elements.css(trait_key ,trait_value); | ||||
Jonathan Frederic
|
r14546 | } | ||
Jonathan Frederic
|
r17177 | } | ||
Jonathan Frederic
|
r14546 | }, | ||
Jonathan Frederic
|
r17728 | update_classes: function (old_classes, new_classes, $el) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Update the DOM classes applied to an element, default to this.$el. | ||||
*/ | ||||
Jonathan Frederic
|
r17728 | if ($el===undefined) { | ||
$el = this.$el; | ||||
} | ||||
Jason Grout
|
r19064 | _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);}) | ||
_.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);}) | ||||
Jonathan Frederic
|
r17720 | }, | ||
Jonathan Frederic
|
r17728 | update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Update the DOM classes applied to the widget based on a single | ||||
* trait's value. | ||||
* | ||||
* Given a trait value classes map, this function automatically | ||||
* handles applying the appropriate classes to the widget element | ||||
* and removing classes that are no longer valid. | ||||
* | ||||
* Parameters | ||||
* ---------- | ||||
* class_map: dictionary | ||||
* Dictionary of trait values to class lists. | ||||
* Example: | ||||
* { | ||||
* success: ['alert', 'alert-success'], | ||||
* info: ['alert', 'alert-info'], | ||||
* warning: ['alert', 'alert-warning'], | ||||
* danger: ['alert', 'alert-danger'] | ||||
* }; | ||||
* trait_name: string | ||||
* Name of the trait to check the value of. | ||||
* previous_trait_value: optional string, default '' | ||||
* Last trait value | ||||
* $el: optional jQuery element handle, defaults to this.$el | ||||
* Element that the classes are applied to. | ||||
*/ | ||||
Jonathan Frederic
|
r17728 | var key = previous_trait_value; | ||
if (key === undefined) { | ||||
key = this.model.previous(trait_name); | ||||
} | ||||
var old_classes = class_map[key] ? class_map[key] : []; | ||||
key = this.model.get(trait_name); | ||||
var new_classes = class_map[key] ? class_map[key] : []; | ||||
this.update_classes(old_classes, new_classes, $el || this.$el); | ||||
}, | ||||
Jonathan Frederic
|
r14546 | _get_selector_element: function (selector) { | ||
Jonathan Frederic
|
r19176 | /** | ||
* Get the elements via the css selector. | ||||
*/ | ||||
Jonathan Frederic
|
r14546 | var elements; | ||
MinRK
|
r14792 | if (!selector) { | ||
Jason Grout
|
r17420 | elements = this.$el; | ||
Jonathan Frederic
|
r14546 | } else { | ||
Jason Grout
|
r17420 | elements = this.$el.find(selector).addBack(selector); | ||
Jonathan Frederic
|
r14546 | } | ||
return elements; | ||||
}, | ||||
Nicholas Bollweg (Nick)
|
r19197 | |||
Nicholas Bollweg (Nick)
|
r19198 | typeset: function(element, text){ | ||
Nicholas Bollweg (Nick)
|
r19235 | utils.typeset.apply(null, arguments); | ||
Nicholas Bollweg (Nick)
|
r19197 | }, | ||
Jonathan Frederic
|
r14546 | }); | ||
Jason Grout
|
r18996 | |||
var ViewList = function(create_view, remove_view, context) { | ||||
Jonathan Frederic
|
r19176 | /** | ||
Jonathan Frederic
|
r19177 | * - create_view and remove_view are default functions called when adding or removing views | ||
* - create_view takes a model and returns a view or a promise for a view for that model | ||||
* - remove_view takes a view and destroys it (including calling `view.remove()`) | ||||
* - each time the update() function is called with a new list, the create and remove | ||||
Jonathan Frederic
|
r19176 | * callbacks will be called in an order so that if you append the views created in the | ||
* create callback and remove the views in the remove callback, you will duplicate | ||||
* the order of the list. | ||||
Jonathan Frederic
|
r19177 | * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter) | ||
* - the context defaults to the created ViewList. If you pass another context, the create and remove | ||||
Jonathan Frederic
|
r19176 | * will be called in that context. | ||
*/ | ||||
Jason Grout
|
r18996 | |||
this.initialize.apply(this, arguments); | ||||
Jonathan Frederic
|
r19023 | }; | ||
Jason Grout
|
r18996 | |||
_.extend(ViewList.prototype, { | ||||
initialize: function(create_view, remove_view, context) { | ||||
this._handler_context = context || this; | ||||
this._models = []; | ||||
Jason Grout
|
r19377 | this.views = []; // list of promises for views | ||
Jason Grout
|
r18996 | this._create_view = create_view; | ||
Jason Grout
|
r19002 | this._remove_view = remove_view || function(view) {view.remove();}; | ||
Jason Grout
|
r18996 | }, | ||
update: function(new_models, create_view, remove_view, context) { | ||||
Jonathan Frederic
|
r19176 | /** | ||
* the create_view, remove_view, and context arguments override the defaults | ||||
* specified when the list is created. | ||||
Jason Grout
|
r19377 | * after this function, the .views attribute is a list of promises for views | ||
* if you want to perform some action on the list of views, do something like | ||||
* `Promise.all(myviewlist.views).then(function(views) {...});` | ||||
Jonathan Frederic
|
r19176 | */ | ||
Jason Grout
|
r18996 | var remove = remove_view || this._remove_view; | ||
var create = create_view || this._create_view; | ||||
Matthias Bussonnier
|
r19739 | context = context || this._handler_context; | ||
Jason Grout
|
r19377 | var i = 0; | ||
// first, skip past the beginning of the lists if they are identical | ||||
for (; i < new_models.length; i++) { | ||||
if (i >= this._models.length || new_models[i] !== this._models[i]) { | ||||
break; | ||||
Jason Grout
|
r18996 | } | ||
Jason Grout
|
r19377 | } | ||
var first_removed = i; | ||||
// Remove the non-matching items from the old list. | ||||
var removed = this.views.splice(first_removed, this.views.length-first_removed); | ||||
for (var j = 0; j < removed.length; j++) { | ||||
removed[j].then(function(view) { | ||||
remove.call(context, view) | ||||
Jason Grout
|
r18996 | }); | ||
Jason Grout
|
r19377 | } | ||
// Add the rest of the new list items. | ||||
for (; i < new_models.length; i++) { | ||||
this.views.push(Promise.resolve(create.call(context, new_models[i]))); | ||||
} | ||||
// make a copy of the input array | ||||
this._models = new_models.slice(); | ||||
Jason Grout
|
r18996 | }, | ||
remove: function() { | ||||
Jonathan Frederic
|
r19176 | /** | ||
* removes every view in the list; convenience function for `.update([])` | ||||
* that should be faster | ||||
* returns a promise that resolves after this removal is done | ||||
*/ | ||||
Jason Grout
|
r18996 | var that = this; | ||
Jason Grout
|
r19407 | return Promise.all(this.views).then(function(views) { | ||
Jason Grout
|
r19087 | for (var i = 0; i < that.views.length; i++) { | ||
Jason Grout
|
r19377 | that._remove_view.call(that._handler_context, views[i]); | ||
Jonathan Frederic
|
r19023 | } | ||
Jason Grout
|
r18996 | that.views = []; | ||
Jason Grout
|
r19377 | that._models = []; | ||
Jason Grout
|
r18996 | }); | ||
}, | ||||
}); | ||||
Jonathan Frederic
|
r17216 | var widget = { | ||
Jonathan Frederic
|
r17198 | 'WidgetModel': WidgetModel, | ||
'WidgetView': WidgetView, | ||||
'DOMWidgetView': DOMWidgetView, | ||||
Jason Grout
|
r18996 | 'ViewList': ViewList, | ||
Jonathan Frederic
|
r17198 | }; | ||
Jonathan Frederic
|
r17216 | |||
// For backwards compatability. | ||||
$.extend(IPython, widget); | ||||
return widget; | ||||
Jonathan Frederic
|
r14546 | }); | ||