@jupyter-widgets/base
Version:
Jupyter interactive widgets
993 lines • 36.9 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import * as utils from './utils';
import * as backbonePatch from './backbone-patch';
import * as Backbone from 'backbone';
import $ from 'jquery';
import { NativeView } from './nativeview';
import { JSONExt } from '@lumino/coreutils';
import { MessageLoop } from '@lumino/messaging';
import { Widget, Panel } from '@lumino/widgets';
import { JUPYTER_WIDGETS_VERSION } from './version';
/**
* The magic key used in the widget graph serialization.
*/
const IPY_MODEL_ = 'IPY_MODEL_';
/**
* Replace model ids with models recursively.
*/
export function unpack_models(value, manager // actually required, but typed to be compatible with ISerializers
) {
if (Array.isArray(value)) {
const unpacked = [];
for (const sub_value of value) {
unpacked.push(unpack_models(sub_value, manager));
}
return Promise.all(unpacked);
}
else if (value instanceof Object && typeof value !== 'string') {
const unpacked = {};
for (const [key, sub_value] of Object.entries(value)) {
unpacked[key] = unpack_models(sub_value, manager);
}
return utils.resolvePromisesDict(unpacked);
}
else if (typeof value === 'string' && value.slice(0, 10) === IPY_MODEL_) {
// get_model returns a promise already
return manager.get_model(value.slice(10, value.length));
}
else {
return Promise.resolve(value);
}
}
/** Replace models with ids recursively.
*
* If the commonly-used `unpack_models` is given as the `deseralize` method,
* pack_models would be the appropriate `serialize`.
* However, the default serialize method will have the same effect, when
* `unpack_models` is used as the deserialize method.
* This is to ensure backwards compatibility, see:
* https://github.com/jupyter-widgets/ipywidgets/pull/3738/commits/f9e27328bb631eb5247a7a6563595d3e655492c7#diff-efb19099381ae8911dd7f69b015a0138d08da7164512c1ee112aa75100bc9be2
*/
export function pack_models(value, widget) {
if (Array.isArray(value)) {
const model_ids = [];
for (const model of value) {
model_ids.push(pack_models(model, widget));
}
return model_ids;
}
else if (value instanceof WidgetModel) {
return `${IPY_MODEL_}${value.model_id}`;
}
else if (value instanceof Object && typeof value !== 'string') {
const packed = {};
for (const [key, sub_value] of Object.entries(value)) {
packed[key] = pack_models(sub_value, widget);
}
return packed;
}
else {
return value;
}
}
export class WidgetModel extends Backbone.Model {
/**
* The default attributes.
*/
defaults() {
return {
_model_module: '@jupyter-widgets/base',
_model_name: 'WidgetModel',
_model_module_version: JUPYTER_WIDGETS_VERSION,
_view_module: '@jupyter-widgets/base',
_view_name: null,
_view_module_version: JUPYTER_WIDGETS_VERSION,
_view_count: null,
};
}
/**
* Test to see if the model has been synced with the server.
*
* #### Notes
* As of backbone 1.1, backbone ignores `patch` if it thinks the
* model has never been pushed.
*/
isNew() {
return false;
}
/**
* Constructor
*
* Initializes a WidgetModel instance. Called by the Backbone constructor.
*
* Parameters
* ----------
* widget_manager : WidgetManager instance
* model_id : string
* An ID unique to this model.
* comm : Comm instance (optional)
*/
initialize(attributes, options) {
this._expectedEchoMsgIds = new Map();
this._attrsToUpdate = new Set();
super.initialize(attributes, options);
// Attributes should be initialized here, since user initialization may depend on it
this.widget_manager = options.widget_manager;
this.model_id = options.model_id;
const comm = options.comm;
this.views = Object.create(null);
this.state_change = Promise.resolve();
this._closed = false;
this._state_lock = null;
this._msg_buffer = null;
this._msg_buffer_callbacks = null;
this._pending_msgs = 0;
// _buffered_state_diff must be created *after* the super.initialize
// call above. See the note in the set() method below.
this._buffered_state_diff = {};
if (comm) {
// Remember comm associated with the model.
this.comm = comm;
// Hook comm messages up to model.
comm.on_close(this._handle_comm_closed.bind(this));
comm.on_msg(this._handle_comm_msg.bind(this));
this.comm_live = true;
}
else {
this.comm_live = false;
}
}
get comm_live() {
return this._comm_live;
}
set comm_live(x) {
this._comm_live = x;
this.trigger('comm_live_update');
}
/**
* Send a custom msg over the comm.
*/
send(content, callbacks, buffers) {
if (this.comm !== undefined) {
const data = { method: 'custom', content: content };
this.comm.send(data, callbacks, {}, buffers);
}
}
/**
* Close model
*
* @param comm_closed - true if the comm is already being closed. If false, the comm will be closed.
*
* @returns - a promise that is fulfilled when all the associated views have been removed.
*/
close(comm_closed = false) {
// can only be closed once.
if (this._closed) {
return Promise.resolve();
}
this._closed = true;
if (this.comm && !comm_closed) {
this.comm.close();
}
this.stopListening();
this.trigger('destroy', this);
if (this.comm) {
delete this.comm;
}
// Delete all views of this model
if (this.views) {
const views = Object.keys(this.views).map((id) => {
return this.views[id].then((view) => view.remove());
});
delete this.views;
return Promise.all(views).then(() => {
return;
});
}
return Promise.resolve();
}
/**
* Handle when a widget comm is closed.
*/
_handle_comm_closed(msg) {
this.trigger('comm:close');
this.close(true);
}
/**
* Handle incoming comm msg.
*/
_handle_comm_msg(msg) {
const data = msg.content.data;
const method = data.method;
switch (method) {
case 'update':
case 'echo_update':
this.state_change = this.state_change
.then(() => {
var _a, _b, _c;
const state = data.state;
const buffer_paths = (_a = data.buffer_paths) !== null && _a !== void 0 ? _a : [];
const buffers = (_c = (_b = msg.buffers) === null || _b === void 0 ? void 0 : _b.slice(0, buffer_paths.length)) !== null && _c !== void 0 ? _c : [];
utils.put_buffers(state, buffer_paths, buffers);
if (msg.parent_header && method === 'echo_update') {
const msgId = msg.parent_header.msg_id;
// we may have echos coming from other clients, we only care about
// dropping echos for which we expected a reply
const expectedEcho = Object.keys(state).filter((attrName) => this._expectedEchoMsgIds.has(attrName));
expectedEcho.forEach((attrName) => {
// Skip echo messages until we get the reply we are expecting.
const isOldMessage = this._expectedEchoMsgIds.get(attrName) !== msgId;
if (isOldMessage) {
// Ignore an echo update that comes before our echo.
delete state[attrName];
}
else {
// we got our echo confirmation, so stop looking for it
this._expectedEchoMsgIds.delete(attrName);
// Start accepting echo updates unless we plan to send out a new state soon
if (this._msg_buffer !== null &&
Object.prototype.hasOwnProperty.call(this._msg_buffer, attrName)) {
delete state[attrName];
}
}
});
}
return this.constructor._deserialize_state(
// Combine the state updates, with preference for kernel updates
state, this.widget_manager);
})
.then((state) => {
this.set_state(state);
})
.catch(utils.reject(`Could not process update msg for model id: ${this.model_id}`, true));
return this.state_change;
case 'custom':
this.trigger('msg:custom', data.content, msg.buffers);
return Promise.resolve();
}
return Promise.resolve();
}
/**
* Handle when a widget is updated from the backend.
*
* This function is meant for internal use only. Values set here will not be propagated on a sync.
*/
set_state(state) {
this._state_lock = state;
try {
this.set(state);
}
catch (e) {
console.error(`Error setting state: ${e instanceof Error ? e.message : e}`);
}
finally {
this._state_lock = null;
}
}
/**
* Get the serializable state of the model.
*
* If drop_default is truthy, attributes that are equal to their default
* values are dropped.
*/
get_state(drop_defaults) {
const fullState = this.attributes;
if (drop_defaults) {
// if defaults is a function, call it
const d = this.defaults;
const defaults = typeof d === 'function' ? d.call(this) : d;
const state = {};
Object.keys(fullState).forEach((key) => {
if (!utils.isEqual(fullState[key], defaults[key])) {
state[key] = fullState[key];
}
});
return state;
}
else {
return Object.assign({}, fullState);
}
}
/**
* Handle status msgs.
*
* execution_state : ('busy', 'idle', 'starting')
*/
_handle_status(msg) {
if (this.comm !== void 0) {
if (msg.content.execution_state === 'idle') {
this._pending_msgs--;
// Sanity check for logic errors that may push this below zero.
if (this._pending_msgs < 0) {
console.error(`Jupyter Widgets message throttle: Pending messages < 0 (=${this._pending_msgs}), which is unexpected. Resetting to 0 to continue.`);
this._pending_msgs = 0; // do not break message throttling in case of unexpected errors
}
// Send buffer if one is waiting and we are below the throttle.
if (this._msg_buffer !== null && this._pending_msgs < 1) {
const msgId = this.send_sync_message(this._msg_buffer, this._msg_buffer_callbacks);
this.rememberLastUpdateFor(msgId);
this._msg_buffer = null;
this._msg_buffer_callbacks = null;
}
}
}
}
/**
* Create msg callbacks for a comm msg.
*/
callbacks(view) {
return this.widget_manager.callbacks(view);
}
/**
* Set one or more values.
*
* We just call the super method, in which val and options are optional.
* Handles both "key", value and {key: value} -style arguments.
*/
set(key, val, options) {
// Call our patched backbone set. See #1642 and #1643.
const return_value = backbonePatch.set.call(this, key, val, options);
// Backbone only remembers the diff of the most recent set()
// operation. Calling set multiple times in a row results in a
// loss of change information. Here we keep our own running diff.
//
// We don't buffer the state set in the constructor (including
// defaults), so we first check to see if we've initialized _buffered_state_diff.
// which happens after the constructor sets attributes at creation.
if (this._buffered_state_diff !== void 0) {
const attrs = this.changedAttributes() || {};
// The state_lock lists attributes that are currently being 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 if
// they haven't changed from their state_lock value.
// The state lock could be null or undefined (if set is being called from
// the initializer).
if (this._state_lock) {
for (const key of Object.keys(this._state_lock)) {
if (attrs[key] === this._state_lock[key]) {
delete attrs[key];
}
}
}
// _buffered_state_diff_synced lists things that have already been sent to the kernel during a top-level call to .set(), so we don't need to buffer these things either.
if (this._buffered_state_diff_synced) {
for (const key of Object.keys(this._buffered_state_diff_synced)) {
if (attrs[key] === this._buffered_state_diff_synced[key]) {
delete attrs[key];
}
}
}
this._buffered_state_diff = utils.assign(this._buffered_state_diff, attrs);
}
// If this ended a top-level call to .set, then reset _buffered_state_diff_synced
if (this._changing === false) {
this._buffered_state_diff_synced = {};
}
return return_value;
}
/**
* Handle sync to the back-end. Called when a model.save() is called.
*
* Make sure a comm exists.
*
* 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.
*
*/
sync(method, model, options = {}) {
// the typing is to return `any` since the super.sync method returns a JqXHR, but we just return false if there is an error.
if (this.comm === undefined) {
throw 'Syncing error: no comm channel defined';
}
const attrs = method === 'patch'
? options.attrs
: model.get_state(options.drop_defaults);
// The state_lock lists attributes that are currently being 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 if
// they haven't changed from their state_lock value.
// The state lock could be null or undefined (if this is triggered
// from the initializer).
if (this._state_lock) {
for (const key of Object.keys(this._state_lock)) {
if (attrs[key] === this._state_lock[key]) {
delete attrs[key];
}
}
}
Object.keys(attrs).forEach((attrName) => {
this._attrsToUpdate.add(attrName);
});
const msgState = this.serialize(attrs);
if (Object.keys(msgState).length > 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...
const callbacks = options.callbacks || this.callbacks();
// Check throttle.
if (this._pending_msgs >= 1) {
// 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 = utils.assign(this._msg_buffer || {}, msgState);
break;
case 'update':
case 'create':
this._msg_buffer = msgState;
break;
default:
throw 'unrecognized syncing method';
}
this._msg_buffer_callbacks = callbacks;
}
else {
// We haven't exceeded the throttle, send the message like
// normal.
const msgId = this.send_sync_message(attrs, callbacks);
this.rememberLastUpdateFor(msgId);
// Since the comm is a one-way communication, assume the message
// arrived and was processed successfully.
// Don't call options.success since we don't have a model back from
// the server. Note that this means we don't have the Backbone
// 'sync' event.
}
}
}
rememberLastUpdateFor(msgId) {
this._attrsToUpdate.forEach((attrName) => {
this._expectedEchoMsgIds.set(attrName, msgId);
});
this._attrsToUpdate = new Set();
}
/**
* Serialize widget state.
*
* A serializer is a function which takes in a state attribute and a widget,
* and synchronously returns a JSONable object. The returned object will
* have toJSON called if possible, and the final result should be a
* primitive object that is a snapshot of the widget state that may have
* binary array buffers.
*/
serialize(state) {
const serializers = this.constructor.serializers ||
JSONExt.emptyObject;
for (const k of Object.keys(state)) {
try {
if (serializers[k] && serializers[k].serialize) {
state[k] = serializers[k].serialize(state[k], this);
}
else {
// the default serializer just deep-copies the object
state[k] = JSON.parse(JSON.stringify(state[k]));
}
if (state[k] && state[k].toJSON) {
state[k] = state[k].toJSON();
}
}
catch (e) {
console.error('Error serializing widget state attribute: ', k);
throw e;
}
}
return state;
}
/**
* Send a sync message to the kernel.
*
* If a message is sent successfully, this returns the message ID of that
* message. Otherwise it returns an empty string
*/
send_sync_message(state, callbacks = {}) {
if (!this.comm) {
return '';
}
try {
// Make a 2-deep copy so we don't modify the caller's callbacks object.
callbacks = {
shell: Object.assign({}, callbacks.shell),
iopub: Object.assign({}, callbacks.iopub),
input: callbacks.input,
};
// Save the caller's status callback so we can call it after we handle the message.
const statuscb = callbacks.iopub.status;
callbacks.iopub.status = (msg) => {
this._handle_status(msg);
if (statuscb) {
statuscb(msg);
}
};
// split out the binary buffers
const split = utils.remove_buffers(state);
const msgId = this.comm.send({
method: 'update',
state: split.state,
buffer_paths: split.buffer_paths,
}, callbacks, {}, split.buffers);
this._pending_msgs++;
return msgId;
}
catch (e) {
console.error('Could not send widget sync message', e);
}
return '';
}
/**
* Push this model's state to the back-end
*
* This invokes a Backbone.Sync.
*/
save_changes(callbacks) {
if (this.comm_live) {
const options = { patch: true };
if (callbacks) {
options.callbacks = callbacks;
}
this.save(this._buffered_state_diff, options);
// If we are currently in a .set() call, save what state we have synced
// to the kernel so we don't buffer it again as we come out of the .set call.
if (this._changing) {
utils.assign(this._buffered_state_diff_synced, this._buffered_state_diff);
}
this._buffered_state_diff = {};
}
}
/**
* 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.
*/
on_some_change(keys, callback, context) {
this.on('change', (...args) => {
if (keys.some(this.hasChanged, this)) {
callback.apply(context, args);
}
}, this);
}
/**
* Serialize the model. See the deserialization function at the top of this file
* and the kernel-side serializer/deserializer.
*/
toJSON(options) {
return `IPY_MODEL_${this.model_id}`;
}
/**
* Returns a promise for the deserialized state. The second argument
* is an instance of widget manager, which is required for the
* deserialization of widget models.
*/
static _deserialize_state(state, manager) {
const serializers = this.serializers;
let deserialized;
if (serializers) {
deserialized = {};
for (const k in state) {
if (serializers[k] && serializers[k].deserialize) {
deserialized[k] = serializers[k].deserialize(state[k], manager);
}
else {
deserialized[k] = state[k];
}
}
}
else {
deserialized = state;
}
return utils.resolvePromisesDict(deserialized);
}
}
export class DOMWidgetModel extends WidgetModel {
defaults() {
return utils.assign(super.defaults(), {
_dom_classes: [],
tabbable: null,
tooltip: null,
// We do not declare defaults for the layout and style attributes.
// Those defaults are constructed on the kernel side and synced here
// as needed, and our code here copes with those attributes being
// undefined. See
// https://github.com/jupyter-widgets/ipywidgets/issues/1620 and
// https://github.com/jupyter-widgets/ipywidgets/pull/1621
});
}
}
DOMWidgetModel.serializers = Object.assign(Object.assign({}, WidgetModel.serializers), { layout: { deserialize: unpack_models }, style: { deserialize: unpack_models } });
export class WidgetView extends NativeView {
/**
* Public constructor.
*/
constructor(options) {
super(options);
}
/**
* Initializer, called at the end of the constructor.
*/
initialize(parameters) {
this.listenTo(this.model, 'change', (model, options) => {
const changed = Object.keys(this.model.changedAttributes() || {});
if (changed[0] === '_view_count' && changed.length === 1) {
// Just the view count was updated
return;
}
this.update(options);
});
this.options = parameters.options;
this.once('remove', () => {
if (typeof this.model.get('_view_count') === 'number') {
this.model.set('_view_count', this.model.get('_view_count') - 1);
this.model.save_changes();
}
});
this.once('displayed', () => {
if (typeof this.model.get('_view_count') === 'number') {
this.model.set('_view_count', this.model.get('_view_count') + 1);
this.model.save_changes();
}
});
this.displayed = new Promise((resolve, reject) => {
this.once('displayed', resolve);
this.model.on('msg:custom', this.handle_message.bind(this));
});
}
/**
* Handle message sent to the front end.
*
* Used to focus or blur the widget.
*/
handle_message(content) {
if (content.do === 'focus') {
this.el.focus();
}
else if (content.do === 'blur') {
this.el.blur();
}
}
/**
* Triggered on model change.
*
* Update view to be consistent with this.model
*/
update(options) {
return;
}
/**
* Render a view
*
* @returns the view or a promise to the view.
*/
render() {
return;
}
create_child_view(child_model, options = {}) {
options = Object.assign({ parent: this }, options);
return this.model.widget_manager
.create_view(child_model, options)
.catch(utils.reject('Could not create child view', true));
}
/**
* Create msg callbacks for a comm msg.
*/
callbacks() {
return this.model.callbacks(this);
}
/**
* Send a custom msg associated with this view.
*/
send(content, buffers) {
this.model.send(content, this.callbacks(), buffers);
}
touch() {
this.model.save_changes(this.callbacks());
}
remove() {
// Raise a remove event when the view is removed.
super.remove();
this.trigger('remove');
return this;
}
}
export class JupyterLuminoWidget extends Widget {
constructor(options) {
const view = options.view;
// Cast as any since we cannot delete a mandatory value
delete options.view;
super(options);
this._view = view;
}
/**
* Dispose the widget.
*
* This causes the view to be destroyed as well with 'remove'
*/
dispose() {
if (this.isDisposed) {
return;
}
super.dispose();
this._view.remove();
this._view = null;
}
/**
* Process the Lumino message.
*
* Any custom Lumino widget used inside a Jupyter widget should override
* the processMessage function like this.
*/
processMessage(msg) {
super.processMessage(msg);
this._view.processLuminoMessage(msg);
}
}
/**
* @deprecated Use {@link JupyterLuminoWidget} instead (Since 8.0).
*/
export const JupyterPhosphorWidget = JupyterLuminoWidget;
export class JupyterLuminoPanelWidget extends Panel {
constructor(options) {
const view = options.view;
delete options.view;
super(options);
this._view = view;
}
/**
* Process the Lumino message.
*
* Any custom Lumino widget used inside a Jupyter widget should override
* the processMessage function like this.
*/
processMessage(msg) {
super.processMessage(msg);
this._view.processLuminoMessage(msg);
}
/**
* Dispose the widget.
*
* This causes the view to be destroyed as well with 'remove'
*/
dispose() {
var _a;
if (this.isDisposed) {
return;
}
super.dispose();
(_a = this._view) === null || _a === void 0 ? void 0 : _a.remove();
this._view = null;
}
}
/**
* @deprecated Use {@link JupyterLuminoPanelWidget} instead (Since 8.0).
*/
export const JupyterPhosphorPanelWidget = JupyterLuminoPanelWidget;
export class DOMWidgetView extends WidgetView {
/**
* Public constructor
*/
initialize(parameters) {
super.initialize(parameters);
this.listenTo(this.model, 'change:_dom_classes', (model, new_classes) => {
const old_classes = model.previous('_dom_classes');
this.update_classes(old_classes, new_classes);
});
this.layoutPromise = Promise.resolve();
this.listenTo(this.model, 'change:layout', (model, value) => {
this.setLayout(value, model.previous('layout'));
});
this.stylePromise = Promise.resolve();
this.listenTo(this.model, 'change:style', (model, value) => {
this.setStyle(value, model.previous('style'));
});
this.displayed.then(() => {
this.update_classes([], this.model.get('_dom_classes'));
this.setLayout(this.model.get('layout'));
this.setStyle(this.model.get('style'));
});
this._comm_live_update();
this.listenTo(this.model, 'comm_live_update', () => {
this._comm_live_update();
});
this.listenTo(this.model, 'change:tooltip', this.updateTooltip);
this.updateTooltip();
}
setLayout(layout, oldLayout) {
if (layout) {
this.layoutPromise = this.layoutPromise.then((oldLayoutView) => {
if (oldLayoutView) {
oldLayoutView.unlayout();
this.stopListening(oldLayoutView.model);
oldLayoutView.remove();
}
return this.create_child_view(layout)
.then((view) => {
// Trigger the displayed event of the child view.
return this.displayed.then(() => {
view.trigger('displayed');
this.listenTo(view.model, 'change', () => {
// Post (asynchronous) so layout changes can take
// effect first.
MessageLoop.postMessage(this.luminoWidget, Widget.ResizeMessage.UnknownSize);
});
MessageLoop.postMessage(this.luminoWidget, Widget.ResizeMessage.UnknownSize);
this.trigger('layout-changed');
return view;
});
})
.catch(utils.reject('Could not add LayoutView to DOMWidgetView', true));
});
}
}
setStyle(style, oldStyle) {
if (style) {
this.stylePromise = this.stylePromise.then((oldStyleView) => {
if (oldStyleView) {
oldStyleView.unstyle();
this.stopListening(oldStyleView.model);
oldStyleView.remove();
}
return this.create_child_view(style)
.then((view) => {
// Trigger the displayed event of the child view.
return this.displayed.then(() => {
view.trigger('displayed');
this.trigger('style-changed');
// Unlike for the layout attribute, style changes don't
// trigger Lumino resize messages.
return view;
});
})
.catch(utils.reject('Could not add styleView to DOMWidgetView', true));
});
}
}
updateTooltip() {
const title = this.model.get('tooltip');
if (!title) {
this.el.removeAttribute('title');
}
else if (this.model.get('description').length === 0) {
this.el.setAttribute('title', title);
}
}
/**
* Update the DOM classes applied to an element, default to this.el.
*/
update_classes(old_classes, new_classes, el) {
if (el === undefined) {
el = this.el;
}
utils.difference(old_classes, new_classes).map(function (c) {
if (el.classList) {
// classList is not supported by IE for svg elements
el.classList.remove(c);
}
else {
el.setAttribute('class', el.getAttribute('class').replace(c, ''));
}
});
utils.difference(new_classes, old_classes).map(function (c) {
if (el.classList) {
// classList is not supported by IE for svg elements
el.classList.add(c);
}
else {
el.setAttribute('class', el.getAttribute('class').concat(' ', c));
}
});
}
/**
* 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.
* el: optional DOM element handle, defaults to this.el
* Element that the classes are applied to.
*/
update_mapped_classes(class_map, trait_name, el) {
let key = this.model.previous(trait_name);
const old_classes = class_map[key] ? class_map[key] : [];
key = this.model.get(trait_name);
const new_classes = class_map[key] ? class_map[key] : [];
this.update_classes(old_classes, new_classes, el || this.el);
}
set_mapped_classes(class_map, trait_name, el) {
const key = this.model.get(trait_name);
const new_classes = class_map[key] ? class_map[key] : [];
this.update_classes([], new_classes, el || this.el);
}
_setElement(el) {
if (this.luminoWidget) {
this.luminoWidget.dispose();
}
this.$el = el instanceof $ ? el : $(el);
this.el = this.$el[0];
this.luminoWidget = new JupyterLuminoWidget({
node: el,
view: this,
});
}
remove() {
if (this.luminoWidget) {
this.luminoWidget.dispose();
}
return super.remove();
}
/**
* @deprecated Use {@link processLuminoMessage} instead (Since 8.0).
*/
processPhosphorMessage(msg) {
this.processLuminoMessage(msg);
}
processLuminoMessage(msg) {
switch (msg.type) {
case 'after-attach':
this.trigger('displayed');
break;
case 'show':
this.trigger('shown');
break;
}
}
_comm_live_update() {
if (this.model.comm_live) {
this.luminoWidget.removeClass('jupyter-widgets-disconnected');
}
else {
this.luminoWidget.addClass('jupyter-widgets-disconnected');
}
}
updateTabindex() {
const tabbable = this.model.get('tabbable');
if (tabbable === true) {
this.el.setAttribute('tabIndex', '0');
}
else if (tabbable === false) {
this.el.setAttribute('tabIndex', '-1');
}
else if (tabbable === null) {
this.el.removeAttribute('tabIndex');
}
}
/**
* @deprecated Use {@link luminoWidget} instead (Since 8.0).
*/
get pWidget() {
return this.luminoWidget;
}
/**
* @deprecated Use {@link luminoWidget} instead (Since 8.0).
*/
set pWidget(value) {
this.luminoWidget = value;
}
}
//# sourceMappingURL=widget.js.map