// Copyright (c) IPython Development Team. // Distributed under the terms of the Modified BSD License. define(["widgets/js/manager", "underscore", "backbone", "jquery", "base/js/utils", "base/js/namespace", ], function(widgetmanager, _, Backbone, $, utils, IPython){ "use strict"; var WidgetModel = Backbone.Model.extend({ constructor: function (widget_manager, model_id, comm) { /** * Constructor * * Creates a WidgetModel instance. * * Parameters * ---------- * widget_manager : WidgetManager instance * model_id : string * An ID unique to this model. * comm : Comm instance (optional) */ this.widget_manager = widget_manager; this.state_change = Promise.resolve(); this._buffered_state_diff = {}; this.pending_msgs = 0; this.msg_buffer = null; this.state_lock = null; this.id = model_id; this.views = {}; this.serializers = {}; this._resolve_received_state = {}; 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)); // Assume the comm is alive. this.set_comm_live(true); } else { this.set_comm_live(false); } // 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); return Backbone.Model.apply(this); }, send: function (content, callbacks, buffers) { /** * Send a custom msg over the comm. */ if (this.comm !== undefined) { var data = {method: 'custom', content: content}; this.comm.send(data, callbacks, {}, buffers); this.pending_msgs++; } }, 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; } var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks()); // Promise that is resolved when a state is received // from the back-end. var that = this; var received_state = new Promise(function(resolve) { that._resolve_received_state[msg_id] = resolve; }); return received_state; }, set_comm_live: function(live) { /** * Change the comm_live state of the model. */ if (this.comm_live === undefined || this.comm_live != live) { this.comm_live = live; this.trigger(live ? 'comm:live' : 'comm:dead', {model: this}); } }, close: function(comm_closed) { /** * Close model */ if (this.comm && !comm_closed) { this.comm.close(); } this.stopListening(); this.trigger('destroy', this); 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. _.each(this.views, function(v, id, views) { v.then(function(view) { view.remove(); delete views[id]; }); }); }, _handle_comm_closed: function (msg) { /** * Handle when a widget is closed. */ this.trigger('comm:close'); this.close(true); }, _handle_comm_msg: function (msg) { /** * Handle incoming comm msg. */ var method = msg.content.data.method; var that = this; switch (method) { case 'update': this.state_change = this.state_change .then(function() { 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 0) { // 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. if (this.pending_msgs >= (this.get('msg_throttle') || 3)) { // 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': this.msg_buffer = $.extend(this.msg_buffer || {}, attrs); break; case 'update': case 'create': 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 // normal. this.send_sync_message(attrs, callbacks); this.pending_msgs++; } } // Since the comm is a one-way communication, assume the message // 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. this._buffered_state_diff = {}; }, send_sync_message: function(attrs, callbacks) { // prepare and send a comm message syncing attrs var that = this; // 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 0) { var trait_key = css[i][1]; var trait_value = css[i][2]; elements.css(trait_key ,trait_value); } } }, update_classes: function (old_classes, new_classes, $el) { /** * Update the DOM classes applied to an element, default to this.$el. */ if ($el===undefined) { $el = this.$el; } _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);}) _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);}) }, update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) { /** * 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. */ 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); }, _get_selector_element: function (selector) { /** * Get the elements via the css selector. */ var elements; if (!selector) { elements = this.$el; } else { elements = this.$el.find(selector).addBack(selector); } return elements; }, typeset: function(element, text){ utils.typeset.apply(null, arguments); }, }); var ViewList = function(create_view, remove_view, context) { /** * - 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 * 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. * - 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 * will be called in that context. */ this.initialize.apply(this, arguments); }; _.extend(ViewList.prototype, { initialize: function(create_view, remove_view, context) { this._handler_context = context || this; this._models = []; this.views = []; // list of promises for views this._create_view = create_view; this._remove_view = remove_view || function(view) {view.remove();}; }, update: function(new_models, create_view, remove_view, context) { /** * the create_view, remove_view, and context arguments override the defaults * specified when the list is created. * 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) {...});` */ var remove = remove_view || this._remove_view; var create = create_view || this._create_view; context = context || this._handler_context; 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; } } 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) }); } // 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(); }, remove: function() { /** * removes every view in the list; convenience function for `.update([])` * that should be faster * returns a promise that resolves after this removal is done */ var that = this; return Promise.all(this.views).then(function(views) { for (var i = 0; i < that.views.length; i++) { that._remove_view.call(that._handler_context, views[i]); } that.views = []; that._models = []; }); }, }); var widget = { 'WidgetModel': WidgetModel, 'WidgetView': WidgetView, 'DOMWidgetView': DOMWidgetView, 'ViewList': ViewList, }; // For backwards compatability. $.extend(IPython, widget); return widget; });