1 | // Copyright (c) Jupyter Development Team.
|
2 | // Distributed under the terms of the Modified BSD License.
|
3 | import * as utils from './utils';
|
4 | import * as backbonePatch from './backbone-patch';
|
5 | import * as Backbone from 'backbone';
|
6 | import $ from 'jquery';
|
7 | import { NativeView } from './nativeview';
|
8 | import { JSONExt } from '@lumino/coreutils';
|
9 | import { MessageLoop } from '@lumino/messaging';
|
10 | import { Widget, Panel } from '@lumino/widgets';
|
11 | import { JUPYTER_WIDGETS_VERSION } from './version';
|
12 | /**
|
13 | * The magic key used in the widget graph serialization.
|
14 | */
|
15 | const IPY_MODEL_ = 'IPY_MODEL_';
|
16 | /**
|
17 | * Replace model ids with models recursively.
|
18 | */
|
19 | export 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 | */
|
52 | export 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 | }
|
73 | export 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 | }
|
593 | export 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 | }
|
608 | DOMWidgetModel.serializers = Object.assign(Object.assign({}, WidgetModel.serializers), { layout: { deserialize: unpack_models }, style: { deserialize: unpack_models } });
|
609 | export 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 | }
|
703 | export 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 | */
|
738 | export const JupyterPhosphorWidget = JupyterLuminoWidget;
|
739 | export 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 | }
|
771 | export 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 |