UNPKG

36.4 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import * as utils from './utils';
4import * as backbonePatch from './backbone-patch';
5import * as Backbone from 'backbone';
6import $ from 'jquery';
7import { NativeView } from './nativeview';
8import { JSONExt } from '@lumino/coreutils';
9import { MessageLoop } from '@lumino/messaging';
10import { Widget, Panel } from '@lumino/widgets';
11import { JUPYTER_WIDGETS_VERSION } from './version';
12/**
13 * The magic key used in the widget graph serialization.
14 */
15const IPY_MODEL_ = 'IPY_MODEL_';
16/**
17 * Replace model ids with models recursively.
18 */
19export function unpack_models(value, manager // actually required, but typed to be compatible with ISerializers
20) {
21 if (Array.isArray(value)) {
22 const unpacked = [];
23 for (const sub_value of value) {
24 unpacked.push(unpack_models(sub_value, manager));
25 }
26 return Promise.all(unpacked);
27 }
28 else if (value instanceof Object && typeof value !== 'string') {
29 const unpacked = {};
30 for (const [key, sub_value] of Object.entries(value)) {
31 unpacked[key] = unpack_models(sub_value, manager);
32 }
33 return utils.resolvePromisesDict(unpacked);
34 }
35 else if (typeof value === 'string' && value.slice(0, 10) === IPY_MODEL_) {
36 // get_model returns a promise already
37 return manager.get_model(value.slice(10, value.length));
38 }
39 else {
40 return Promise.resolve(value);
41 }
42}
43/** Replace models with ids recursively.
44 *
45 * If the commonly-used `unpack_models` is given as the `deseralize` method,
46 * pack_models would be the appropriate `serialize`.
47 * However, the default serialize method will have the same effect, when
48 * `unpack_models` is used as the deserialize method.
49 * This is to ensure backwards compatibility, see:
50 * https://github.com/jupyter-widgets/ipywidgets/pull/3738/commits/f9e27328bb631eb5247a7a6563595d3e655492c7#diff-efb19099381ae8911dd7f69b015a0138d08da7164512c1ee112aa75100bc9be2
51 */
52export function pack_models(value, widget) {
53 if (Array.isArray(value)) {
54 const model_ids = [];
55 for (const model of value) {
56 model_ids.push(pack_models(model, widget));
57 }
58 return model_ids;
59 }
60 else if (value instanceof WidgetModel) {
61 return `${IPY_MODEL_}${value.model_id}`;
62 }
63 else if (value instanceof Object && typeof value !== 'string') {
64 const packed = {};
65 for (const [key, sub_value] of Object.entries(value)) {
66 packed[key] = pack_models(sub_value, widget);
67 }
68 }
69 else {
70 return value;
71 }
72}
73export class WidgetModel extends Backbone.Model {
74 /**
75 * The default attributes.
76 */
77 defaults() {
78 return {
79 _model_module: '@jupyter-widgets/base',
80 _model_name: 'WidgetModel',
81 _model_module_version: JUPYTER_WIDGETS_VERSION,
82 _view_module: '@jupyter-widgets/base',
83 _view_name: null,
84 _view_module_version: JUPYTER_WIDGETS_VERSION,
85 _view_count: null,
86 };
87 }
88 /**
89 * Test to see if the model has been synced with the server.
90 *
91 * #### Notes
92 * As of backbone 1.1, backbone ignores `patch` if it thinks the
93 * model has never been pushed.
94 */
95 isNew() {
96 return false;
97 }
98 /**
99 * Constructor
100 *
101 * Initializes a WidgetModel instance. Called by the Backbone constructor.
102 *
103 * Parameters
104 * ----------
105 * widget_manager : WidgetManager instance
106 * model_id : string
107 * An ID unique to this model.
108 * comm : Comm instance (optional)
109 */
110 initialize(attributes, options) {
111 this._expectedEchoMsgIds = new Map();
112 this._attrsToUpdate = new Set();
113 super.initialize(attributes, options);
114 // Attributes should be initialized here, since user initialization may depend on it
115 this.widget_manager = options.widget_manager;
116 this.model_id = options.model_id;
117 const comm = options.comm;
118 this.views = Object.create(null);
119 this.state_change = Promise.resolve();
120 this._closed = false;
121 this._state_lock = null;
122 this._msg_buffer = null;
123 this._msg_buffer_callbacks = null;
124 this._pending_msgs = 0;
125 // _buffered_state_diff must be created *after* the super.initialize
126 // call above. See the note in the set() method below.
127 this._buffered_state_diff = {};
128 if (comm) {
129 // Remember comm associated with the model.
130 this.comm = comm;
131 // Hook comm messages up to model.
132 comm.on_close(this._handle_comm_closed.bind(this));
133 comm.on_msg(this._handle_comm_msg.bind(this));
134 this.comm_live = true;
135 }
136 else {
137 this.comm_live = false;
138 }
139 }
140 get comm_live() {
141 return this._comm_live;
142 }
143 set comm_live(x) {
144 this._comm_live = x;
145 this.trigger('comm_live_update');
146 }
147 /**
148 * Send a custom msg over the comm.
149 */
150 send(content, callbacks, buffers) {
151 if (this.comm !== undefined) {
152 const data = { method: 'custom', content: content };
153 this.comm.send(data, callbacks, {}, buffers);
154 }
155 }
156 /**
157 * Close model
158 *
159 * @param comm_closed - true if the comm is already being closed. If false, the comm will be closed.
160 *
161 * @returns - a promise that is fulfilled when all the associated views have been removed.
162 */
163 close(comm_closed = false) {
164 // can only be closed once.
165 if (this._closed) {
166 return Promise.resolve();
167 }
168 this._closed = true;
169 if (this.comm && !comm_closed) {
170 this.comm.close();
171 }
172 this.stopListening();
173 this.trigger('destroy', this);
174 if (this.comm) {
175 delete this.comm;
176 }
177 // Delete all views of this model
178 if (this.views) {
179 const views = Object.keys(this.views).map((id) => {
180 return this.views[id].then((view) => view.remove());
181 });
182 delete this.views;
183 return Promise.all(views).then(() => {
184 return;
185 });
186 }
187 return Promise.resolve();
188 }
189 /**
190 * Handle when a widget comm is closed.
191 */
192 _handle_comm_closed(msg) {
193 this.trigger('comm:close');
194 this.close(true);
195 }
196 /**
197 * Handle incoming comm msg.
198 */
199 _handle_comm_msg(msg) {
200 const data = msg.content.data;
201 const method = data.method;
202 switch (method) {
203 case 'update':
204 case 'echo_update':
205 this.state_change = this.state_change
206 .then(() => {
207 var _a, _b, _c;
208 const state = data.state;
209 const buffer_paths = (_a = data.buffer_paths) !== null && _a !== void 0 ? _a : [];
210 const buffers = (_c = (_b = msg.buffers) === null || _b === void 0 ? void 0 : _b.slice(0, buffer_paths.length)) !== null && _c !== void 0 ? _c : [];
211 utils.put_buffers(state, buffer_paths, buffers);
212 if (msg.parent_header && method === 'echo_update') {
213 const msgId = msg.parent_header.msg_id;
214 // we may have echos coming from other clients, we only care about
215 // dropping echos for which we expected a reply
216 const expectedEcho = Object.keys(state).filter((attrName) => this._expectedEchoMsgIds.has(attrName));
217 expectedEcho.forEach((attrName) => {
218 // Skip echo messages until we get the reply we are expecting.
219 const isOldMessage = this._expectedEchoMsgIds.get(attrName) !== msgId;
220 if (isOldMessage) {
221 // Ignore an echo update that comes before our echo.
222 delete state[attrName];
223 }
224 else {
225 // we got our echo confirmation, so stop looking for it
226 this._expectedEchoMsgIds.delete(attrName);
227 // Start accepting echo updates unless we plan to send out a new state soon
228 if (this._msg_buffer !== null &&
229 Object.prototype.hasOwnProperty.call(this._msg_buffer, attrName)) {
230 delete state[attrName];
231 }
232 }
233 });
234 }
235 return this.constructor._deserialize_state(
236 // Combine the state updates, with preference for kernel updates
237 state, this.widget_manager);
238 })
239 .then((state) => {
240 this.set_state(state);
241 })
242 .catch(utils.reject(`Could not process update msg for model id: ${this.model_id}`, true));
243 return this.state_change;
244 case 'custom':
245 this.trigger('msg:custom', data.content, msg.buffers);
246 return Promise.resolve();
247 }
248 return Promise.resolve();
249 }
250 /**
251 * Handle when a widget is updated from the backend.
252 *
253 * This function is meant for internal use only. Values set here will not be propagated on a sync.
254 */
255 set_state(state) {
256 this._state_lock = state;
257 try {
258 this.set(state);
259 }
260 catch (e) {
261 console.error(`Error setting state: ${e instanceof Error ? e.message : e}`);
262 }
263 finally {
264 this._state_lock = null;
265 }
266 }
267 /**
268 * Get the serializable state of the model.
269 *
270 * If drop_default is truthy, attributes that are equal to their default
271 * values are dropped.
272 */
273 get_state(drop_defaults) {
274 const fullState = this.attributes;
275 if (drop_defaults) {
276 // if defaults is a function, call it
277 const d = this.defaults;
278 const defaults = typeof d === 'function' ? d.call(this) : d;
279 const state = {};
280 Object.keys(fullState).forEach((key) => {
281 if (!utils.isEqual(fullState[key], defaults[key])) {
282 state[key] = fullState[key];
283 }
284 });
285 return state;
286 }
287 else {
288 return Object.assign({}, fullState);
289 }
290 }
291 /**
292 * Handle status msgs.
293 *
294 * execution_state : ('busy', 'idle', 'starting')
295 */
296 _handle_status(msg) {
297 if (this.comm !== void 0) {
298 if (msg.content.execution_state === 'idle') {
299 this._pending_msgs--;
300 // Sanity check for logic errors that may push this below zero.
301 if (this._pending_msgs < 0) {
302 console.error(`Jupyter Widgets message throttle: Pending messages < 0 (=${this._pending_msgs}), which is unexpected. Resetting to 0 to continue.`);
303 this._pending_msgs = 0; // do not break message throttling in case of unexpected errors
304 }
305 // Send buffer if one is waiting and we are below the throttle.
306 if (this._msg_buffer !== null && this._pending_msgs < 1) {
307 const msgId = this.send_sync_message(this._msg_buffer, this._msg_buffer_callbacks);
308 this.rememberLastUpdateFor(msgId);
309 this._msg_buffer = null;
310 this._msg_buffer_callbacks = null;
311 }
312 }
313 }
314 }
315 /**
316 * Create msg callbacks for a comm msg.
317 */
318 callbacks(view) {
319 return this.widget_manager.callbacks(view);
320 }
321 /**
322 * Set one or more values.
323 *
324 * We just call the super method, in which val and options are optional.
325 * Handles both "key", value and {key: value} -style arguments.
326 */
327 set(key, val, options) {
328 // Call our patched backbone set. See #1642 and #1643.
329 const return_value = backbonePatch.set.call(this, key, val, options);
330 // Backbone only remembers the diff of the most recent set()
331 // operation. Calling set multiple times in a row results in a
332 // loss of change information. Here we keep our own running diff.
333 //
334 // We don't buffer the state set in the constructor (including
335 // defaults), so we first check to see if we've initialized _buffered_state_diff.
336 // which happens after the constructor sets attributes at creation.
337 if (this._buffered_state_diff !== void 0) {
338 const attrs = this.changedAttributes() || {};
339 // The state_lock lists attributes that are currently being changed
340 // right now from a kernel message. We don't want to send these
341 // non-changes back to the kernel, so we delete them out of attrs if
342 // they haven't changed from their state_lock value.
343 // The state lock could be null or undefined (if set is being called from
344 // the initializer).
345 if (this._state_lock) {
346 for (const key of Object.keys(this._state_lock)) {
347 if (attrs[key] === this._state_lock[key]) {
348 delete attrs[key];
349 }
350 }
351 }
352 // _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.
353 if (this._buffered_state_diff_synced) {
354 for (const key of Object.keys(this._buffered_state_diff_synced)) {
355 if (attrs[key] === this._buffered_state_diff_synced[key]) {
356 delete attrs[key];
357 }
358 }
359 }
360 this._buffered_state_diff = utils.assign(this._buffered_state_diff, attrs);
361 }
362 // If this ended a top-level call to .set, then reset _buffered_state_diff_synced
363 if (this._changing === false) {
364 this._buffered_state_diff_synced = {};
365 }
366 return return_value;
367 }
368 /**
369 * Handle sync to the back-end. Called when a model.save() is called.
370 *
371 * Make sure a comm exists.
372 *
373 * Parameters
374 * ----------
375 * method : create, update, patch, delete, read
376 * create/update always send the full attribute set
377 * patch - only send attributes listed in options.attrs, and if we
378 * are queuing up messages, combine with previous messages that have
379 * not been sent yet
380 * model : the model we are syncing
381 * will normally be the same as `this`
382 * options : dict
383 * the `attrs` key, if it exists, gives an {attr: value} dict that
384 * should be synced, otherwise, sync all attributes.
385 *
386 */
387 sync(method, model, options = {}) {
388 // the typing is to return `any` since the super.sync method returns a JqXHR, but we just return false if there is an error.
389 if (this.comm === undefined) {
390 throw 'Syncing error: no comm channel defined';
391 }
392 const attrs = method === 'patch'
393 ? options.attrs
394 : model.get_state(options.drop_defaults);
395 // The state_lock lists attributes that are currently being changed
396 // right now from a kernel message. We don't want to send these
397 // non-changes back to the kernel, so we delete them out of attrs if
398 // they haven't changed from their state_lock value.
399 // The state lock could be null or undefined (if this is triggered
400 // from the initializer).
401 if (this._state_lock) {
402 for (const key of Object.keys(this._state_lock)) {
403 if (attrs[key] === this._state_lock[key]) {
404 delete attrs[key];
405 }
406 }
407 }
408 Object.keys(attrs).forEach((attrName) => {
409 this._attrsToUpdate.add(attrName);
410 });
411 const msgState = this.serialize(attrs);
412 if (Object.keys(msgState).length > 0) {
413 // If this message was sent via backbone itself, it will not
414 // have any callbacks. It's important that we create callbacks
415 // so we can listen for status messages, etc...
416 const callbacks = options.callbacks || this.callbacks();
417 // Check throttle.
418 if (this._pending_msgs >= 1) {
419 // The throttle has been exceeded, buffer the current msg so
420 // it can be sent once the kernel has finished processing
421 // some of the existing messages.
422 // Combine updates if it is a 'patch' sync, otherwise replace updates
423 switch (method) {
424 case 'patch':
425 this._msg_buffer = utils.assign(this._msg_buffer || {}, msgState);
426 break;
427 case 'update':
428 case 'create':
429 this._msg_buffer = msgState;
430 break;
431 default:
432 throw 'unrecognized syncing method';
433 }
434 this._msg_buffer_callbacks = callbacks;
435 }
436 else {
437 // We haven't exceeded the throttle, send the message like
438 // normal.
439 const msgId = this.send_sync_message(attrs, callbacks);
440 this.rememberLastUpdateFor(msgId);
441 // Since the comm is a one-way communication, assume the message
442 // arrived and was processed successfully.
443 // Don't call options.success since we don't have a model back from
444 // the server. Note that this means we don't have the Backbone
445 // 'sync' event.
446 }
447 }
448 }
449 rememberLastUpdateFor(msgId) {
450 this._attrsToUpdate.forEach((attrName) => {
451 this._expectedEchoMsgIds.set(attrName, msgId);
452 });
453 this._attrsToUpdate = new Set();
454 }
455 /**
456 * Serialize widget state.
457 *
458 * A serializer is a function which takes in a state attribute and a widget,
459 * and synchronously returns a JSONable object. The returned object will
460 * have toJSON called if possible, and the final result should be a
461 * primitive object that is a snapshot of the widget state that may have
462 * binary array buffers.
463 */
464 serialize(state) {
465 const serializers = this.constructor.serializers ||
466 JSONExt.emptyObject;
467 for (const k of Object.keys(state)) {
468 try {
469 if (serializers[k] && serializers[k].serialize) {
470 state[k] = serializers[k].serialize(state[k], this);
471 }
472 else {
473 // the default serializer just deep-copies the object
474 state[k] = JSON.parse(JSON.stringify(state[k]));
475 }
476 if (state[k] && state[k].toJSON) {
477 state[k] = state[k].toJSON();
478 }
479 }
480 catch (e) {
481 console.error('Error serializing widget state attribute: ', k);
482 throw e;
483 }
484 }
485 return state;
486 }
487 /**
488 * Send a sync message to the kernel.
489 *
490 * If a message is sent successfully, this returns the message ID of that
491 * message. Otherwise it returns an empty string
492 */
493 send_sync_message(state, callbacks = {}) {
494 if (!this.comm) {
495 return '';
496 }
497 try {
498 // Make a 2-deep copy so we don't modify the caller's callbacks object.
499 callbacks = {
500 shell: Object.assign({}, callbacks.shell),
501 iopub: Object.assign({}, callbacks.iopub),
502 input: callbacks.input,
503 };
504 // Save the caller's status callback so we can call it after we handle the message.
505 const statuscb = callbacks.iopub.status;
506 callbacks.iopub.status = (msg) => {
507 this._handle_status(msg);
508 if (statuscb) {
509 statuscb(msg);
510 }
511 };
512 // split out the binary buffers
513 const split = utils.remove_buffers(state);
514 const msgId = this.comm.send({
515 method: 'update',
516 state: split.state,
517 buffer_paths: split.buffer_paths,
518 }, callbacks, {}, split.buffers);
519 this._pending_msgs++;
520 return msgId;
521 }
522 catch (e) {
523 console.error('Could not send widget sync message', e);
524 }
525 return '';
526 }
527 /**
528 * Push this model's state to the back-end
529 *
530 * This invokes a Backbone.Sync.
531 */
532 save_changes(callbacks) {
533 if (this.comm_live) {
534 const options = { patch: true };
535 if (callbacks) {
536 options.callbacks = callbacks;
537 }
538 this.save(this._buffered_state_diff, options);
539 // If we are currently in a .set() call, save what state we have synced
540 // to the kernel so we don't buffer it again as we come out of the .set call.
541 if (this._changing) {
542 utils.assign(this._buffered_state_diff_synced, this._buffered_state_diff);
543 }
544 this._buffered_state_diff = {};
545 }
546 }
547 /**
548 * on_some_change(['key1', 'key2'], foo, context) differs from
549 * on('change:key1 change:key2', foo, context).
550 * If the widget attributes key1 and key2 are both modified,
551 * the second form will result in foo being called twice
552 * while the first will call foo only once.
553 */
554 on_some_change(keys, callback, context) {
555 this.on('change', (...args) => {
556 if (keys.some(this.hasChanged, this)) {
557 callback.apply(context, args);
558 }
559 }, this);
560 }
561 /**
562 * Serialize the model. See the deserialization function at the top of this file
563 * and the kernel-side serializer/deserializer.
564 */
565 toJSON(options) {
566 return `IPY_MODEL_${this.model_id}`;
567 }
568 /**
569 * Returns a promise for the deserialized state. The second argument
570 * is an instance of widget manager, which is required for the
571 * deserialization of widget models.
572 */
573 static _deserialize_state(state, manager) {
574 const serializers = this.serializers;
575 let deserialized;
576 if (serializers) {
577 deserialized = {};
578 for (const k in state) {
579 if (serializers[k] && serializers[k].deserialize) {
580 deserialized[k] = serializers[k].deserialize(state[k], manager);
581 }
582 else {
583 deserialized[k] = state[k];
584 }
585 }
586 }
587 else {
588 deserialized = state;
589 }
590 return utils.resolvePromisesDict(deserialized);
591 }
592}
593export class DOMWidgetModel extends WidgetModel {
594 defaults() {
595 return utils.assign(super.defaults(), {
596 _dom_classes: [],
597 tabbable: null,
598 tooltip: null,
599 // We do not declare defaults for the layout and style attributes.
600 // Those defaults are constructed on the kernel side and synced here
601 // as needed, and our code here copes with those attributes being
602 // undefined. See
603 // https://github.com/jupyter-widgets/ipywidgets/issues/1620 and
604 // https://github.com/jupyter-widgets/ipywidgets/pull/1621
605 });
606 }
607}
608DOMWidgetModel.serializers = Object.assign(Object.assign({}, WidgetModel.serializers), { layout: { deserialize: unpack_models }, style: { deserialize: unpack_models } });
609export class WidgetView extends NativeView {
610 /**
611 * Public constructor.
612 */
613 constructor(options) {
614 super(options);
615 }
616 /**
617 * Initializer, called at the end of the constructor.
618 */
619 initialize(parameters) {
620 this.listenTo(this.model, 'change', (model, options) => {
621 const changed = Object.keys(this.model.changedAttributes() || {});
622 if (changed[0] === '_view_count' && changed.length === 1) {
623 // Just the view count was updated
624 return;
625 }
626 this.update(options);
627 });
628 this.options = parameters.options;
629 this.once('remove', () => {
630 if (typeof this.model.get('_view_count') === 'number') {
631 this.model.set('_view_count', this.model.get('_view_count') - 1);
632 this.model.save_changes();
633 }
634 });
635 this.once('displayed', () => {
636 if (typeof this.model.get('_view_count') === 'number') {
637 this.model.set('_view_count', this.model.get('_view_count') + 1);
638 this.model.save_changes();
639 }
640 });
641 this.displayed = new Promise((resolve, reject) => {
642 this.once('displayed', resolve);
643 this.model.on('msg:custom', this.handle_message.bind(this));
644 });
645 }
646 /**
647 * Handle message sent to the front end.
648 *
649 * Used to focus or blur the widget.
650 */
651 handle_message(content) {
652 if (content.do === 'focus') {
653 this.el.focus();
654 }
655 else if (content.do === 'blur') {
656 this.el.blur();
657 }
658 }
659 /**
660 * Triggered on model change.
661 *
662 * Update view to be consistent with this.model
663 */
664 update(options) {
665 return;
666 }
667 /**
668 * Render a view
669 *
670 * @returns the view or a promise to the view.
671 */
672 render() {
673 return;
674 }
675 create_child_view(child_model, options = {}) {
676 options = Object.assign({ parent: this }, options);
677 return this.model.widget_manager
678 .create_view(child_model, options)
679 .catch(utils.reject('Could not create child view', true));
680 }
681 /**
682 * Create msg callbacks for a comm msg.
683 */
684 callbacks() {
685 return this.model.callbacks(this);
686 }
687 /**
688 * Send a custom msg associated with this view.
689 */
690 send(content, buffers) {
691 this.model.send(content, this.callbacks(), buffers);
692 }
693 touch() {
694 this.model.save_changes(this.callbacks());
695 }
696 remove() {
697 // Raise a remove event when the view is removed.
698 super.remove();
699 this.trigger('remove');
700 return this;
701 }
702}
703export class JupyterLuminoWidget extends Widget {
704 constructor(options) {
705 const view = options.view;
706 // Cast as any since we cannot delete a mandatory value
707 delete options.view;
708 super(options);
709 this._view = view;
710 }
711 /**
712 * Dispose the widget.
713 *
714 * This causes the view to be destroyed as well with 'remove'
715 */
716 dispose() {
717 if (this.isDisposed) {
718 return;
719 }
720 super.dispose();
721 this._view.remove();
722 this._view = null;
723 }
724 /**
725 * Process the Lumino message.
726 *
727 * Any custom Lumino widget used inside a Jupyter widget should override
728 * the processMessage function like this.
729 */
730 processMessage(msg) {
731 super.processMessage(msg);
732 this._view.processLuminoMessage(msg);
733 }
734}
735/**
736 * @deprecated Use {@link JupyterLuminoWidget} instead (Since 8.0).
737 */
738export const JupyterPhosphorWidget = JupyterLuminoWidget;
739export class JupyterLuminoPanelWidget extends Panel {
740 constructor(options) {
741 const view = options.view;
742 delete options.view;
743 super(options);
744 this._view = view;
745 }
746 /**
747 * Process the Lumino message.
748 *
749 * Any custom Lumino widget used inside a Jupyter widget should override
750 * the processMessage function like this.
751 */
752 processMessage(msg) {
753 super.processMessage(msg);
754 this._view.processLuminoMessage(msg);
755 }
756 /**
757 * Dispose the widget.
758 *
759 * This causes the view to be destroyed as well with 'remove'
760 */
761 dispose() {
762 var _a;
763 if (this.isDisposed) {
764 return;
765 }
766 super.dispose();
767 (_a = this._view) === null || _a === void 0 ? void 0 : _a.remove();
768 this._view = null;
769 }
770}
771export class DOMWidgetView extends WidgetView {
772 /**
773 * Public constructor
774 */
775 initialize(parameters) {
776 super.initialize(parameters);
777 this.listenTo(this.model, 'change:_dom_classes', (model, new_classes) => {
778 const old_classes = model.previous('_dom_classes');
779 this.update_classes(old_classes, new_classes);
780 });
781 this.layoutPromise = Promise.resolve();
782 this.listenTo(this.model, 'change:layout', (model, value) => {
783 this.setLayout(value, model.previous('layout'));
784 });
785 this.stylePromise = Promise.resolve();
786 this.listenTo(this.model, 'change:style', (model, value) => {
787 this.setStyle(value, model.previous('style'));
788 });
789 this.displayed.then(() => {
790 this.update_classes([], this.model.get('_dom_classes'));
791 this.setLayout(this.model.get('layout'));
792 this.setStyle(this.model.get('style'));
793 });
794 this._comm_live_update();
795 this.listenTo(this.model, 'comm_live_update', () => {
796 this._comm_live_update();
797 });
798 this.listenTo(this.model, 'change:tooltip', this.updateTooltip);
799 this.updateTooltip();
800 }
801 setLayout(layout, oldLayout) {
802 if (layout) {
803 this.layoutPromise = this.layoutPromise.then((oldLayoutView) => {
804 if (oldLayoutView) {
805 oldLayoutView.unlayout();
806 this.stopListening(oldLayoutView.model);
807 oldLayoutView.remove();
808 }
809 return this.create_child_view(layout)
810 .then((view) => {
811 // Trigger the displayed event of the child view.
812 return this.displayed.then(() => {
813 view.trigger('displayed');
814 this.listenTo(view.model, 'change', () => {
815 // Post (asynchronous) so layout changes can take
816 // effect first.
817 MessageLoop.postMessage(this.luminoWidget, Widget.ResizeMessage.UnknownSize);
818 });
819 MessageLoop.postMessage(this.luminoWidget, Widget.ResizeMessage.UnknownSize);
820 this.trigger('layout-changed');
821 return view;
822 });
823 })
824 .catch(utils.reject('Could not add LayoutView to DOMWidgetView', true));
825 });
826 }
827 }
828 setStyle(style, oldStyle) {
829 if (style) {
830 this.stylePromise = this.stylePromise.then((oldStyleView) => {
831 if (oldStyleView) {
832 oldStyleView.unstyle();
833 this.stopListening(oldStyleView.model);
834 oldStyleView.remove();
835 }
836 return this.create_child_view(style)
837 .then((view) => {
838 // Trigger the displayed event of the child view.
839 return this.displayed.then(() => {
840 view.trigger('displayed');
841 this.trigger('style-changed');
842 // Unlike for the layout attribute, style changes don't
843 // trigger Lumino resize messages.
844 return view;
845 });
846 })
847 .catch(utils.reject('Could not add styleView to DOMWidgetView', true));
848 });
849 }
850 }
851 updateTooltip() {
852 const title = this.model.get('tooltip');
853 if (!title) {
854 this.el.removeAttribute('title');
855 }
856 else if (this.model.get('description').length === 0) {
857 this.el.setAttribute('title', title);
858 }
859 }
860 /**
861 * Update the DOM classes applied to an element, default to this.el.
862 */
863 update_classes(old_classes, new_classes, el) {
864 if (el === undefined) {
865 el = this.el;
866 }
867 utils.difference(old_classes, new_classes).map(function (c) {
868 if (el.classList) {
869 // classList is not supported by IE for svg elements
870 el.classList.remove(c);
871 }
872 else {
873 el.setAttribute('class', el.getAttribute('class').replace(c, ''));
874 }
875 });
876 utils.difference(new_classes, old_classes).map(function (c) {
877 if (el.classList) {
878 // classList is not supported by IE for svg elements
879 el.classList.add(c);
880 }
881 else {
882 el.setAttribute('class', el.getAttribute('class').concat(' ', c));
883 }
884 });
885 }
886 /**
887 * Update the DOM classes applied to the widget based on a single
888 * trait's value.
889 *
890 * Given a trait value classes map, this function automatically
891 * handles applying the appropriate classes to the widget element
892 * and removing classes that are no longer valid.
893 *
894 * Parameters
895 * ----------
896 * class_map: dictionary
897 * Dictionary of trait values to class lists.
898 * Example:
899 * {
900 * success: ['alert', 'alert-success'],
901 * info: ['alert', 'alert-info'],
902 * warning: ['alert', 'alert-warning'],
903 * danger: ['alert', 'alert-danger']
904 * };
905 * trait_name: string
906 * Name of the trait to check the value of.
907 * el: optional DOM element handle, defaults to this.el
908 * Element that the classes are applied to.
909 */
910 update_mapped_classes(class_map, trait_name, el) {
911 let key = this.model.previous(trait_name);
912 const old_classes = class_map[key] ? class_map[key] : [];
913 key = this.model.get(trait_name);
914 const new_classes = class_map[key] ? class_map[key] : [];
915 this.update_classes(old_classes, new_classes, el || this.el);
916 }
917 set_mapped_classes(class_map, trait_name, el) {
918 const key = this.model.get(trait_name);
919 const new_classes = class_map[key] ? class_map[key] : [];
920 this.update_classes([], new_classes, el || this.el);
921 }
922 _setElement(el) {
923 if (this.luminoWidget) {
924 this.luminoWidget.dispose();
925 }
926 this.$el = el instanceof $ ? el : $(el);
927 this.el = this.$el[0];
928 this.luminoWidget = new JupyterLuminoWidget({
929 node: el,
930 view: this,
931 });
932 }
933 remove() {
934 if (this.luminoWidget) {
935 this.luminoWidget.dispose();
936 }
937 return super.remove();
938 }
939 processLuminoMessage(msg) {
940 switch (msg.type) {
941 case 'after-attach':
942 this.trigger('displayed');
943 break;
944 case 'show':
945 this.trigger('shown');
946 break;
947 }
948 }
949 _comm_live_update() {
950 if (this.model.comm_live) {
951 this.luminoWidget.removeClass('jupyter-widgets-disconnected');
952 }
953 else {
954 this.luminoWidget.addClass('jupyter-widgets-disconnected');
955 }
956 }
957 updateTabindex() {
958 const tabbable = this.model.get('tabbable');
959 if (tabbable === true) {
960 this.el.setAttribute('tabIndex', '0');
961 }
962 else if (tabbable === false) {
963 this.el.setAttribute('tabIndex', '-1');
964 }
965 else if (tabbable === null) {
966 this.el.removeAttribute('tabIndex');
967 }
968 }
969 /**
970 * @deprecated Use {@link luminoWidget} instead (Since 8.0).
971 */
972 get pWidget() {
973 return this.luminoWidget;
974 }
975}
976//# sourceMappingURL=widget.js.map
\No newline at end of file