Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"test:default": "echo \"No test specified\""
},
"dependencies": {
"@jupyter-widgets/html-manager": "^0.20.1",
"@jupyter-widgets/html-manager": "^1.0.14",
"@types/rstudio-shiny": "https://github.com/rstudio/shiny#main",
"base64-arraybuffer": "^1.0.2",
"font-awesome": "^4.7.0"
Expand Down
95 changes: 46 additions & 49 deletions js/src/_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,64 +16,61 @@ window.addEventListener("load", () => {
// Let the world know about a value change so the Shiny input binding
// can subscribe to it (and thus call getValue() whenever that happens)
class InputManager extends HTMLManager {
display_view(msg, view, options): ReturnType<typeof HTMLManager.prototype.display_view> {
async display_view(viewOrPromise: any, el: HTMLElement): Promise<void> {
const view = await viewOrPromise;
await super.display_view(view, el);

return super.display_view(msg, view, options).then((view) => {
// Get the Shiny input container element for this view
const $el_input = view.$el.parents(INPUT_SELECTOR);

// Get the Shiny input container element for this view
const $el_input = view.$el.parents(INPUT_SELECTOR);

// At least currently, ipywidgets have a tagify method, meaning they can
// be directly statically rendered (i.e., without a input_ipywidget() container)
if ($el_input.length == 0) {
return;
}

// Most "input-like" widgets use the value property to encode their current value,
// but some multiple selection widgets (e.g., RadioButtons) use the index property
// instead.
let val = view.model.get("value");
if (val === undefined) {
val = view.model.get("index");
}
// At least currently, ipywidgets have a tagify method, meaning they can
// be directly statically rendered (i.e., without a input_ipywidget() container)
if ($el_input.length == 0) {
return;
}

// Checkbox() apparently doesn't have a value/index property
// on the model on the initial render (but does in the change event,
// so this seems like an ipywidgets bug???)
if (val === undefined && view.hasOwnProperty("checkbox")) {
val = view.checkbox.checked;
}
// Most "input-like" widgets use the value property to encode their current value,
// but some multiple selection widgets (e.g., RadioButtons) use the index property
// instead.
let val = view.model.get("value");
if (val === undefined) {
val = view.model.get("index");
}

// Button() doesn't have a value/index property, and clicking it doesn't trigger
// a change event, so we do that ourselves
if (val === undefined && view.tagName === "button") {
val = 0;
view.$el[0].addEventListener("click", () => {
val++;
_doChangeEvent($el_input[0], val);
});
}
// Checkbox() apparently doesn't have a value/index property
// on the model on the initial render (but does in the change event,
// so this seems like an ipywidgets bug???)
if (val === undefined && view.hasOwnProperty("checkbox")) {
val = view.checkbox.checked;
}

// Mock a change event now so that we know Shiny binding has a chance to
// read the initial value. Also, do it on the next tick since the
// binding hasn't had a chance to subscribe to the change event yet.
setTimeout(() => { _doChangeEvent($el_input[0], val) }, 0);

// Relay changes to the model to the Shiny input binding
view.model.on('change', (x) => {
let val;
if (x.attributes.hasOwnProperty("value")) {
val = x.attributes.value;
} else if (x.attributes.hasOwnProperty("index")) {
val = x.attributes.index;
} else {
throw new Error("Unknown change event" + JSON.stringify(x.attributes));
}
// Button() doesn't have a value/index property, and clicking it doesn't trigger
// a change event, so we do that ourselves
if (val === undefined && view.tagName === "button") {
val = 0;
view.$el[0].addEventListener("click", () => {
val++;
_doChangeEvent($el_input[0], val);
});
}

// Mock a change event now so that we know Shiny binding has a chance to
// read the initial value. Also, do it on the next tick since the
// binding hasn't had a chance to subscribe to the change event yet.
setTimeout(() => { _doChangeEvent($el_input[0], val) }, 0);

// Relay changes to the model to the Shiny input binding
view.model.on('change', (x: any) => {
let val;
if (x.attributes.hasOwnProperty("value")) {
val = x.attributes.value;
} else if (x.attributes.hasOwnProperty("index")) {
val = x.attributes.index;
} else {
throw new Error("Unknown change event" + JSON.stringify(x.attributes));
}
_doChangeEvent($el_input[0], val);
});

}
}

Expand Down
63 changes: 39 additions & 24 deletions js/src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,36 @@ import type { ErrorsMessageValue } from 'rstudio-shiny/srcts/types/src/shiny/shi
******************************************************************************/

class OutputManager extends HTMLManager {
// In a soon-to-be-released version of @jupyter-widgets/html-manager,
// display_view()'s first "dummy" argument will be removed... this shim simply
// makes it so that our manager can work with either version
// https://github.com/jupyter-widgets/ipywidgets/commit/159bbe4#diff-45c126b24c3c43d2cee5313364805c025e911c4721d45ff8a68356a215bfb6c8R42-R43
async display_view(view: any, options: { el: HTMLElement; }): Promise<any> {
const n_args = super.display_view.length
if (n_args === 3) {
return super.display_view({}, view, options)
} else {
// @ts-ignore
return super.display_view(view, options)
// Shiny delivers comm_open messages one at a time with microtask breaks
// between them. A parent model's deserialization may reference a child model
// whose comm_open hasn't been processed yet. The base get_model throws
// synchronously for missing models, so we override it to wait for late
// arrivals (with a timeout to avoid silent hangs).
private _modelWaiters = new Map<string, Array<(p: Promise<any>) => void>>();

async get_model(model_id: string): Promise<any> {
if (this.has_model(model_id)) {
return super.get_model(model_id);
}
return new Promise<any>((resolve, reject) => {
const waiters = this._modelWaiters.get(model_id) || [];
waiters.push(resolve);
this._modelWaiters.set(model_id, waiters);
setTimeout(
() => reject(new Error(`Timeout waiting for widget model ${model_id}`)),
10000
);
});
}

register_model(model_id: string, modelPromise: Promise<any>): void {
super.register_model(model_id, modelPromise);
const waiters = this._modelWaiters.get(model_id);
if (waiters) {
this._modelWaiters.delete(model_id);
for (const resolve of waiters) {
resolve(modelPromise);
}
}
}
}
Expand Down Expand Up @@ -102,12 +121,9 @@ class IPyWidgetOutput extends Shiny.OutputBinding {
// At this time point, we should've already handled an 'open' message, and so
// the model should be ready to use
const model = await manager.get_model(data.model_id);
if (!model) {
throw new Error(`No model found for id ${data.model_id}`);
}

const view = await manager.create_view(model, {});
await manager.display_view(view, {el: el});
const view = await manager.create_view(model, { el });
await manager.display_view(view, el);

// Don't allow more than one .lmWidget container, which can happen
// when the view is displayed more than once
Expand Down Expand Up @@ -188,6 +204,7 @@ class IPyWidgetOutput extends Shiny.OutputBinding {
const quakWidget = impl.shadowRoot.querySelector(".quak") as HTMLElement;
quakWidget.style.maxHeight = "unset";
}

}
_doResize(): void {
// Trigger resize event to force layout (setTimeout() is needed for altair)
Expand Down Expand Up @@ -235,13 +252,12 @@ Shiny.addCustomMessageHandler("shinywidgets_comm_open", (msg_txt) => {
Shiny.addCustomMessageHandler("shinywidgets_comm_msg", async (msg_txt) => {
const msg = jsonParse(msg_txt);
const id = msg.content.comm_id;
const model = manager.get_model(id);
if (!model) {
if (!manager.has_model(id)) {
console.error(`Couldn't handle message for model ${id} because it doesn't exist.`);
return;
}
try {
const m = await model;
const m = await manager.get_model(id);
// @ts-ignore for some reason IClassicComm doesn't have this method, but we do
m.comm.handle_msg(msg);
} catch (err) {
Expand All @@ -254,14 +270,13 @@ Shiny.addCustomMessageHandler("shinywidgets_comm_msg", async (msg_txt) => {
Shiny.addCustomMessageHandler("shinywidgets_comm_close", async (msg_txt) => {
const msg = jsonParse(msg_txt);
const id = msg.content.comm_id;
const model = manager.get_model(id);
if (!model) {
if (!manager.has_model(id)) {
console.error(`Couldn't close model ${id} because it doesn't exist.`);
return;
}

try {
const m = await model;
const m = await manager.get_model(id);

// Some widget views need explicit teardown before model.close() removes them.
if (m.views) {
Expand All @@ -276,8 +291,8 @@ Shiny.addCustomMessageHandler("shinywidgets_comm_close", async (msg_txt) => {
v.destroy();
// Clearing the back-reference prevents later teardown from touching a
// model that is already being closed.
delete v.model;
v.remove();
delete (v as any).model;
(v as any).remove();
}


Expand Down
Loading
Loading