Add Shiny.initializedPromise (#4063)

* Convert Shiny from interface to class

* Remove unused global Shiny type

* Add prettier plugin for organizing imports

* Disable eslint indentation rule

* Simplify types

* Add Shiny.connectedPromise and Shiny.sessionInitPromise

* Fix typing issue

* Move prettier plugin to devDependencies

* Rename Shiny class to ShinyClass, and export type

* Remove global Shiny type; use internal imports

* Small code cleanup

* Move initShiny() function into ShinyClass

* Rebuild type files

* Raise error if window.Shiny already exists

* Rename promises

* Add InitStatusPromise class

* `yarn build` (GitHub Actions)

* Update news

* Remove isConnected

* Update yarn.lock

* Rename isInitialized to initializedPromise

* Rebuild shiny.js

* `yarn build` (GitHub Actions)

* Update NEWS

---------

Co-authored-by: wch <wch@users.noreply.github.com>
This commit is contained in:
Winston Chang
2024-07-23 22:11:20 -05:00
committed by GitHub
parent 25c40967da
commit bb89cf9235
28 changed files with 4015 additions and 4066 deletions

View File

@@ -35,10 +35,6 @@ rules:
default-case: default-case:
- error - error
indent:
- error
- 2
- SwitchCase: 1
linebreak-style: linebreak-style:
- error - error
- unix - unix

View File

@@ -2,6 +2,10 @@
## New features and improvements ## New features and improvements
* The client-side TypeScript code for Shiny has been refactored so that the `Shiny` object is now an instance of class `ShinyClass`. (#4063)
* In TypeScript, the `Shiny` object has a new property `initializedPromise`, which is a Promise-like object that can be `await`ed or chained with `.then()`. This Promise-like object corresponds to the `shiny:sessioninitialized` JavaScript event, but is easier to use because it can be used both before and after the events have occurred. (#4063)
* Added new functions, `useBusyIndicators()` and `busyIndicatorOptions()`, for enabling and customizing busy indication. Busy indicators provide a visual cue to users when the server is busy calculating outputs or otherwise serving requests to the client. When enabled, a spinner is shown on each calculating/recalculating output, and a pulsing banner is shown at the top of the page when the app is otherwise busy. (#4040) * Added new functions, `useBusyIndicators()` and `busyIndicatorOptions()`, for enabling and customizing busy indication. Busy indicators provide a visual cue to users when the server is busy calculating outputs or otherwise serving requests to the client. When enabled, a spinner is shown on each calculating/recalculating output, and a pulsing banner is shown at the top of the page when the app is otherwise busy. (#4040)
* Output bindings now include the `.recalculating` CSS class when they are first bound, up until the first render. This makes it possible/easier to show progress indication when the output is calculating for the first time. (#4039) * Output bindings now include the `.recalculating` CSS class when they are first bound, up until the first render. This makes it possible/easier to show progress indication when the output is calculating for the first time. (#4039)

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,19 +2,12 @@
// Project: Shiny <https://shiny.rstudio.com/> // Project: Shiny <https://shiny.rstudio.com/>
// Definitions by: RStudio <https://www.rstudio.com/> // Definitions by: RStudio <https://www.rstudio.com/>
import type { Shiny as RStudioShiny } from "../src/shiny/index"; import type { ShinyClass } from "../src/shiny/index";
declare global { declare global {
// Tell Shiny variable globally exists
// eslint-disable-next-line @typescript-eslint/naming-convention
const Shiny: RStudioShiny;
// Tell window.Shiny exists // Tell window.Shiny exists
interface Window { interface Window {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
Shiny: RStudioShiny; Shiny: ShinyClass;
} }
// Make `Shiny` a globally available type definition. (No need to import the type)
type Shiny = RStudioShiny;
} }

View File

@@ -18,11 +18,10 @@ import { TextInputBinding } from "./text";
import { TextareaInputBinding } from "./textarea"; import { TextareaInputBinding } from "./textarea";
// TODO-barret make this an init method // TODO-barret make this an init method
type InitInputBindings = { function initInputBindings(): {
inputBindings: BindingRegistry<InputBinding>; inputBindings: BindingRegistry<InputBinding>;
fileInputBinding: FileInputBinding; fileInputBinding: FileInputBinding;
}; } {
function initInputBindings(): InitInputBindings {
const inputBindings = new BindingRegistry<InputBinding>(); const inputBindings = new BindingRegistry<InputBinding>();
inputBindings.register(new TextInputBinding(), "shiny.textInput"); inputBindings.register(new TextInputBinding(), "shiny.textInput");

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { Shiny } from "..";
import { ShinyClientError } from "../shiny/error"; import { ShinyClientError } from "../shiny/error";
const buttonStyles = css` const buttonStyles = css`

View File

@@ -1,3 +1,4 @@
import { init } from "./initialize"; import { init } from "./initialize";
export { Shiny, type ShinyClass } from "./initialize";
init(); init();

View File

@@ -2,15 +2,20 @@ import { determineBrowserInfo } from "./browser";
import { disableFormSubmission } from "./disableForm"; import { disableFormSubmission } from "./disableForm";
import { trackHistory } from "./history"; import { trackHistory } from "./history";
import { setShiny } from "../shiny"; import { ShinyClass } from "../shiny";
import { setUserAgent } from "../utils/userAgent"; import { setUserAgent } from "../utils/userAgent";
import { windowShiny } from "../window/libraries";
import { windowUserAgent } from "../window/userAgent"; import { windowUserAgent } from "../window/userAgent";
import { initReactlog } from "../shiny/reactlog"; import { initReactlog } from "../shiny/reactlog";
// eslint-disable-next-line @typescript-eslint/naming-convention
let Shiny: ShinyClass;
function init(): void { function init(): void {
setShiny(windowShiny()); if (window.Shiny) {
throw new Error("Trying to create window.Shiny, but it already exists!");
}
Shiny = window.Shiny = new ShinyClass();
setUserAgent(windowUserAgent()); // before determineBrowserInfo() setUserAgent(windowUserAgent()); // before determineBrowserInfo()
determineBrowserInfo(); determineBrowserInfo();
@@ -21,4 +26,4 @@ function init(): void {
initReactlog(); initReactlog();
} }
export { init }; export { init, Shiny, type ShinyClass };

View File

@@ -1,4 +1,5 @@
import $ from "jquery"; import $ from "jquery";
import { Shiny } from "..";
import type { InputBinding, OutputBinding } from "../bindings"; import type { InputBinding, OutputBinding } from "../bindings";
import { OutputBindingAdapter } from "../bindings/outputAdapter"; import { OutputBindingAdapter } from "../bindings/outputAdapter";
import type { BindingRegistry } from "../bindings/registry"; import type { BindingRegistry } from "../bindings/registry";

View File

@@ -3,10 +3,33 @@ import $ from "jquery";
import { InputBinding, OutputBinding } from "../bindings"; import { InputBinding, OutputBinding } from "../bindings";
import { initInputBindings } from "../bindings/input"; import { initInputBindings } from "../bindings/input";
import { initOutputBindings } from "../bindings/output"; import { initOutputBindings } from "../bindings/output";
import type { BindingRegistry } from "../bindings/registry";
import { showErrorInClientConsole } from "../components/errorConsole"; import { showErrorInClientConsole } from "../components/errorConsole";
import { resetBrush } from "../imageutils/resetBrush"; import { resetBrush } from "../imageutils/resetBrush";
import { $escape, compareVersion } from "../utils"; import type { InputPolicy } from "../inputPolicies";
import { initShiny } from "./init"; import {
InputBatchSender,
InputDeferDecorator,
InputEventDecorator,
InputNoResendDecorator,
InputRateDecorator,
InputValidateDecorator,
} from "../inputPolicies";
import type { InputPolicyOpts } from "../inputPolicies/inputPolicy";
import { addDefaultInputOpts } from "../inputPolicies/inputValidateDecorator";
import { debounce, Debouncer } from "../time";
import {
$escape,
compareVersion,
getComputedLinkColor,
getStyle,
hasDefinedProperty,
mapValues,
pixelRatio,
} from "../utils";
import { createInitStatus, type InitStatusPromise } from "../utils/promise";
import type { BindInputsCtx, BindScope } from "./bind";
import { bindAll, unbindAll, _bindAll } from "./bind";
import type { import type {
shinyBindAll, shinyBindAll,
shinyForgetLastInputValue, shinyForgetLastInputValue,
@@ -14,11 +37,12 @@ import type {
shinySetInputValue, shinySetInputValue,
shinyUnbindAll, shinyUnbindAll,
} from "./initedMethods"; } from "./initedMethods";
import { setFileInputBinding } from "./initedMethods"; import { setFileInputBinding, setShinyObj } from "./initedMethods";
import { removeModal, showModal } from "./modal"; import { removeModal, showModal } from "./modal";
import { removeNotification, showNotification } from "./notifications"; import { removeNotification, showNotification } from "./notifications";
import { hideReconnectDialog, showReconnectDialog } from "./reconnectDialog"; import { hideReconnectDialog, showReconnectDialog } from "./reconnectDialog";
import { import {
registerDependency,
renderContent, renderContent,
renderContentAsync, renderContentAsync,
renderDependencies, renderDependencies,
@@ -26,17 +50,18 @@ import {
renderHtml, renderHtml,
renderHtmlAsync, renderHtmlAsync,
} from "./render"; } from "./render";
import type { Handler, ShinyApp } from "./shinyapp"; import { sendImageSizeFns } from "./sendImageSize";
import { addCustomMessageHandler } from "./shinyapp"; import { addCustomMessageHandler, ShinyApp, type Handler } from "./shinyapp";
import { registerNames as singletonsRegisterNames } from "./singletons";
interface Shiny { class ShinyClass {
version: string; version: string;
$escape: typeof $escape; $escape: typeof $escape;
compareVersion: typeof compareVersion; compareVersion: typeof compareVersion;
inputBindings: ReturnType<typeof initInputBindings>["inputBindings"]; inputBindings: BindingRegistry<InputBinding>;
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
InputBinding: typeof InputBinding; InputBinding: typeof InputBinding;
outputBindings: ReturnType<typeof initOutputBindings>["outputBindings"]; outputBindings: BindingRegistry<OutputBinding>;
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
OutputBinding: typeof OutputBinding; OutputBinding: typeof OutputBinding;
resetBrush: typeof resetBrush; resetBrush: typeof resetBrush;
@@ -45,7 +70,6 @@ interface Shiny {
remove: typeof removeNotification; remove: typeof removeNotification;
}; };
modal: { show: typeof showModal; remove: typeof removeModal }; modal: { show: typeof showModal; remove: typeof removeModal };
createSocket?: () => WebSocket;
showReconnectDialog: typeof showReconnectDialog; showReconnectDialog: typeof showReconnectDialog;
hideReconnectDialog: typeof hideReconnectDialog; hideReconnectDialog: typeof hideReconnectDialog;
renderDependenciesAsync: typeof renderDependenciesAsync; renderDependenciesAsync: typeof renderDependenciesAsync;
@@ -54,9 +78,12 @@ interface Shiny {
renderContent: typeof renderContent; renderContent: typeof renderContent;
renderHtmlAsync: typeof renderHtmlAsync; renderHtmlAsync: typeof renderHtmlAsync;
renderHtml: typeof renderHtml; renderHtml: typeof renderHtml;
user: string;
progressHandlers?: ShinyApp["progressHandlers"];
addCustomMessageHandler: typeof addCustomMessageHandler; addCustomMessageHandler: typeof addCustomMessageHandler;
// The following are added in the initialization, by initShiny()
createSocket?: () => WebSocket;
user?: string;
progressHandlers?: ShinyApp["progressHandlers"];
shinyapp?: ShinyApp; shinyapp?: ShinyApp;
setInputValue?: typeof shinySetInputValue; setInputValue?: typeof shinySetInputValue;
onInputChange?: typeof shinySetInputValue; onInputChange?: typeof shinySetInputValue;
@@ -65,77 +92,629 @@ interface Shiny {
unbindAll?: typeof shinyUnbindAll; unbindAll?: typeof shinyUnbindAll;
initializeInputs?: typeof shinyInitializeInputs; initializeInputs?: typeof shinyInitializeInputs;
// Promise-like object that is resolved after initialization.
initializedPromise: InitStatusPromise<void>;
// Eventually deprecate // Eventually deprecate
// For old-style custom messages - should deprecate and migrate to new // For old-style custom messages - should deprecate and migrate to new
oncustommessage?: Handler; oncustommessage?: Handler;
constructor() {
// `process.env.SHINY_VERSION` is overwritten to the Shiny version at build time.
// During testing, the `Shiny.version` will be `"development"`
this.version = process.env.SHINY_VERSION || "development";
const { inputBindings, fileInputBinding } = initInputBindings();
const { outputBindings } = initOutputBindings();
setFileInputBinding(fileInputBinding);
this.$escape = $escape;
this.compareVersion = compareVersion;
this.inputBindings = inputBindings;
this.InputBinding = InputBinding;
this.outputBindings = outputBindings;
this.OutputBinding = OutputBinding;
this.resetBrush = resetBrush;
this.notifications = {
show: showNotification,
remove: removeNotification,
};
this.modal = { show: showModal, remove: removeModal };
this.addCustomMessageHandler = addCustomMessageHandler;
this.showReconnectDialog = showReconnectDialog;
this.hideReconnectDialog = hideReconnectDialog;
this.renderDependenciesAsync = renderDependenciesAsync;
this.renderDependencies = renderDependencies;
this.renderContentAsync = renderContentAsync;
this.renderContent = renderContent;
this.renderHtmlAsync = renderHtmlAsync;
this.renderHtml = renderHtml;
this.initializedPromise = createInitStatus<void>();
$(() => {
// Init Shiny a little later than document ready, so user code can
// run first (i.e. to register bindings)
setTimeout(async () => {
try {
await this.initialize();
} catch (e) {
showErrorInClientConsole(e);
throw e;
}
}, 1);
});
}
/** /**
* Method to check if Shiny is running in development mode. By packaging as a * Method to check if Shiny is running in development mode. By packaging as a
* method, we can we can avoid needing to look for the `__SHINY_DEV_MODE__` * method, we can we can avoid needing to look for the `__SHINY_DEV_MODE__`
* variable in the global scope. * variable in the global scope.
* @returns `true` if Shiny is running in development mode, `false` otherwise. * @returns `true` if Shiny is running in development mode, `false` otherwise.
*/ */
inDevMode: () => boolean; inDevMode(): boolean {
}
let windowShiny: Shiny;
function setShiny(windowShiny_: Shiny): void {
windowShiny = windowShiny_;
// `process.env.SHINY_VERSION` is overwritten to the Shiny version at build time.
// During testing, the `Shiny.version` will be `"development"`
windowShiny.version = process.env.SHINY_VERSION || "development";
const { inputBindings, fileInputBinding } = initInputBindings();
const { outputBindings } = initOutputBindings();
// set variable to be retrieved later
setFileInputBinding(fileInputBinding);
windowShiny.$escape = $escape;
windowShiny.compareVersion = compareVersion;
windowShiny.inputBindings = inputBindings;
windowShiny.InputBinding = InputBinding;
windowShiny.outputBindings = outputBindings;
windowShiny.OutputBinding = OutputBinding;
windowShiny.resetBrush = resetBrush;
windowShiny.notifications = {
show: showNotification,
remove: removeNotification,
};
windowShiny.modal = { show: showModal, remove: removeModal };
windowShiny.addCustomMessageHandler = addCustomMessageHandler;
windowShiny.showReconnectDialog = showReconnectDialog;
windowShiny.hideReconnectDialog = hideReconnectDialog;
windowShiny.renderDependenciesAsync = renderDependenciesAsync;
windowShiny.renderDependencies = renderDependencies;
windowShiny.renderContentAsync = renderContentAsync;
windowShiny.renderContent = renderContent;
windowShiny.renderHtmlAsync = renderHtmlAsync;
windowShiny.renderHtml = renderHtml;
windowShiny.inDevMode = () => {
if ("__SHINY_DEV_MODE__" in window) if ("__SHINY_DEV_MODE__" in window)
return Boolean(window.__SHINY_DEV_MODE__); return Boolean(window.__SHINY_DEV_MODE__);
return false; return false;
}; }
$(function () { async initialize(): Promise<void> {
// Init Shiny a little later than document ready, so user code can setShinyObj(this);
// run first (i.e. to register bindings) this.shinyapp = new ShinyApp();
setTimeout(async function () { const shinyapp = this.shinyapp;
try {
await initShiny(windowShiny); this.progressHandlers = shinyapp.progressHandlers;
} catch (e) {
showErrorInClientConsole(e); const inputBatchSender = new InputBatchSender(shinyapp);
throw e; const inputsNoResend = new InputNoResendDecorator(inputBatchSender);
const inputsEvent = new InputEventDecorator(inputsNoResend);
const inputsRate = new InputRateDecorator(inputsEvent);
const inputsDefer = new InputDeferDecorator(inputsEvent);
let target: InputPolicy;
if ($('input[type="submit"], button[type="submit"]').length > 0) {
// If there is a submit button on the page, use defer decorator
target = inputsDefer;
$('input[type="submit"], button[type="submit"]').each(function () {
$(this).click(function (event) {
event.preventDefault();
inputsDefer.submit();
});
});
} else {
// By default, use rate decorator
target = inputsRate;
}
const inputs = new InputValidateDecorator(target);
this.setInputValue = this.onInputChange = function (
name: string,
value: unknown,
opts: Partial<InputPolicyOpts> = {}
): void {
const newOpts = addDefaultInputOpts(opts);
inputs.setInput(name, value, newOpts);
};
// By default, Shiny deduplicates input value changes; that is, if
// `setInputValue` is called with the same value as the input already
// has, the call is ignored (unless opts.priority = "event"). Calling
// `forgetLastInputValue` tells Shiny that the very next call to
// `setInputValue` for this input id shouldn't be ignored, even if it
// is a dupe of the existing value.
this.forgetLastInputValue = function (name) {
inputsNoResend.forget(name);
};
// MUST be called after `setShiny()`
const inputBindings = this.inputBindings;
const outputBindings = this.outputBindings;
function shinyBindCtx(): BindInputsCtx {
return {
inputs,
inputsRate,
sendOutputHiddenState,
maybeAddThemeObserver,
inputBindings,
outputBindings,
initDeferredIframes,
};
}
this.bindAll = async function (scope: BindScope) {
await bindAll(shinyBindCtx(), scope);
};
this.unbindAll = function (scope: BindScope, includeSelf = false) {
unbindAll(shinyBindCtx(), scope, includeSelf);
};
// Calls .initialize() for all of the input objects in all input bindings,
// in the given scope.
function initializeInputs(scope: BindScope = document.documentElement) {
const bindings = inputBindings.getBindings();
// Iterate over all bindings
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i].binding;
const inputObjects = binding.find(scope);
if (inputObjects) {
// Iterate over all input objects for this binding
for (let j = 0; j < inputObjects.length; j++) {
const $inputObjectJ = $(inputObjects[j]);
if (!$inputObjectJ.data("_shiny_initialized")) {
$inputObjectJ.data("_shiny_initialized", true);
binding.initialize(inputObjects[j]);
}
}
}
} }
}, 1); }
this.initializeInputs = initializeInputs;
function getIdFromEl(el: HTMLElement) {
const $el = $(el);
const bindingAdapter = $el.data("shiny-output-binding");
if (!bindingAdapter) return null;
else return bindingAdapter.getId();
}
// Initialize all input objects in the document, before binding
initializeInputs(document.documentElement);
// The input values returned by _bindAll() each have a structure like this:
// { value: 123, opts: { ... } }
// We want to only keep the value. This is because when the initialValues is
// passed to ShinyApp.connect(), the ShinyApp object stores the
// initialValues object for the duration of the session, and the opts may
// have a reference to the DOM element, which would prevent it from being
// GC'd.
const initialValues = mapValues(
await _bindAll(shinyBindCtx(), document.documentElement),
(x) => x.value
);
// The server needs to know the size of each image and plot output element,
// in case it is auto-sizing
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
function () {
const id = getIdFromEl(this),
rect = this.getBoundingClientRect();
if (rect.width !== 0 || rect.height !== 0) {
initialValues[".clientdata_output_" + id + "_width"] = rect.width;
initialValues[".clientdata_output_" + id + "_height"] = rect.height;
}
}
);
function getComputedBgColor(
el: HTMLElement | null
): string | null | undefined {
if (!el) {
// Top of document, can't recurse further
return null;
}
const bgColor = getStyle(el, "background-color");
if (!bgColor) return bgColor;
const m = bgColor.match(
/^rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/
);
if (bgColor === "transparent" || (m && parseFloat(m[4]) === 0)) {
// No background color on this element. See if it has a background image.
const bgImage = getStyle(el, "background-image");
if (bgImage && bgImage !== "none") {
// Failed to detect background color, since it has a background image
return null;
} else {
// Recurse
return getComputedBgColor(el.parentElement);
}
}
return bgColor;
}
function getComputedFont(el: HTMLElement) {
const fontFamily = getStyle(el, "font-family");
const fontSize = getStyle(el, "font-size");
return {
families: fontFamily?.replace(/"/g, "").split(", "),
size: fontSize,
};
}
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
function () {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const el = this;
const id = getIdFromEl(el);
initialValues[".clientdata_output_" + id + "_bg"] =
getComputedBgColor(el);
initialValues[".clientdata_output_" + id + "_fg"] = getStyle(
el,
"color"
);
initialValues[".clientdata_output_" + id + "_accent"] =
getComputedLinkColor(el);
initialValues[".clientdata_output_" + id + "_font"] =
getComputedFont(el);
maybeAddThemeObserver(el);
}
);
// Resend computed styles if *an output element's* class or style attribute changes.
// This gives us some level of confidence that getCurrentOutputInfo() will be
// properly invalidated if output container is mutated; but unfortunately,
// we don't have a reasonable way to detect change in *inherited* styles
// (other than session$setCurrentTheme())
// https://github.com/rstudio/shiny/issues/3196
// https://github.com/rstudio/shiny/issues/2998
function maybeAddThemeObserver(el: HTMLElement): void {
if (!window.MutationObserver) {
return; // IE10 and lower
}
const cl = el.classList;
const reportTheme =
cl.contains("shiny-image-output") ||
cl.contains("shiny-plot-output") ||
cl.contains("shiny-report-theme");
if (!reportTheme) {
return;
}
const $el = $(el);
if ($el.data("shiny-theme-observer")) {
return; // i.e., observer is already observing
}
const observerCallback = new Debouncer(null, () => doSendTheme(el), 100);
const observer = new MutationObserver(() =>
observerCallback.normalCall()
);
const config = { attributes: true, attributeFilter: ["style", "class"] };
observer.observe(el, config);
$el.data("shiny-theme-observer", observer);
}
function doSendTheme(el: HTMLElement): void {
// Sending theme info on error isn't necessary (it'd add an unnecessary additional round-trip)
if (el.classList.contains("shiny-output-error")) {
return;
}
const id = getIdFromEl(el);
inputs.setInput(
".clientdata_output_" + id + "_bg",
getComputedBgColor(el)
);
inputs.setInput(
".clientdata_output_" + id + "_fg",
getStyle(el, "color")
);
inputs.setInput(
".clientdata_output_" + id + "_accent",
getComputedLinkColor(el)
);
inputs.setInput(
".clientdata_output_" + id + "_font",
getComputedFont(el)
);
}
function doSendImageSize() {
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
function () {
const id = getIdFromEl(this),
rect = this.getBoundingClientRect();
if (rect.width !== 0 || rect.height !== 0) {
inputs.setInput(".clientdata_output_" + id + "_width", rect.width);
inputs.setInput(
".clientdata_output_" + id + "_height",
rect.height
);
}
}
);
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
function () {
doSendTheme(this);
}
);
$(".shiny-bound-output").each(function () {
const $this = $(this),
binding = $this.data("shiny-output-binding");
$this.trigger({
type: "shiny:visualchange",
// @ts-expect-error; Can not remove info on a established, malformed Event object
visible: !isHidden(this),
binding: binding,
});
binding.onResize();
});
}
sendImageSizeFns.setImageSend(inputBatchSender, doSendImageSize);
// Return true if the object or one of its ancestors in the DOM tree has
// style='display:none'; otherwise return false.
function isHidden(obj: HTMLElement | null): boolean {
// null means we've hit the top of the tree. If width or height is
// non-zero, then we know that no ancestor has display:none.
if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
return false;
} else if (getStyle(obj, "display") === "none") {
return true;
} else {
return isHidden(obj.parentNode as HTMLElement | null);
}
}
let lastKnownVisibleOutputs: { [key: string]: boolean } = {};
// Set initial state of outputs to hidden, if needed
$(".shiny-bound-output").each(function () {
const id = getIdFromEl(this);
if (isHidden(this)) {
initialValues[".clientdata_output_" + id + "_hidden"] = true;
} else {
lastKnownVisibleOutputs[id] = true;
initialValues[".clientdata_output_" + id + "_hidden"] = false;
}
});
// Send update when hidden state changes
function doSendOutputHiddenState() {
const visibleOutputs: { [key: string]: boolean } = {};
$(".shiny-bound-output").each(function () {
const id = getIdFromEl(this);
delete lastKnownVisibleOutputs[id];
// Assume that the object is hidden when width and height are 0
const hidden = isHidden(this),
evt = {
type: "shiny:visualchange",
visible: !hidden,
};
if (hidden) {
inputs.setInput(".clientdata_output_" + id + "_hidden", true);
} else {
visibleOutputs[id] = true;
inputs.setInput(".clientdata_output_" + id + "_hidden", false);
}
const $this = $(this);
// @ts-expect-error; Can not remove info on a established, malformed Event object
evt.binding = $this.data("shiny-output-binding");
// @ts-expect-error; Can not remove info on a established, malformed Event object
$this.trigger(evt);
});
// Anything left in lastKnownVisibleOutputs is orphaned
for (const name in lastKnownVisibleOutputs) {
if (hasDefinedProperty(lastKnownVisibleOutputs, name))
inputs.setInput(".clientdata_output_" + name + "_hidden", true);
}
// Update the visible outputs for next time
lastKnownVisibleOutputs = visibleOutputs;
}
// sendOutputHiddenState gets called each time DOM elements are shown or
// hidden. This can be in the hundreds or thousands of times at startup.
// We'll debounce it, so that we do the actual work once per tick.
const sendOutputHiddenStateDebouncer = new Debouncer(
null,
doSendOutputHiddenState,
0
);
function sendOutputHiddenState() {
sendOutputHiddenStateDebouncer.normalCall();
}
// We need to make sure doSendOutputHiddenState actually gets called before
// the inputBatchSender sends data to the server. The lastChanceCallback
// here does that - if the debouncer has a pending call, flush it.
inputBatchSender.lastChanceCallback.push(function () {
if (sendOutputHiddenStateDebouncer.isPending())
sendOutputHiddenStateDebouncer.immediateCall();
});
// Given a namespace and a handler function, return a function that invokes
// the handler only when e's namespace matches. For example, if the
// namespace is "bs", it would match when e.namespace is "bs" or "bs.tab".
// If the namespace is "bs.tab", it would match for "bs.tab", but not "bs".
function filterEventsByNamespace(
namespace: string,
handler: (...handlerArgs: any[]) => void,
...args: any[]
) {
const namespaceArr = namespace.split(".");
return function (this: HTMLElement, e: JQuery.TriggeredEvent) {
const eventNamespace = e.namespace?.split(".") ?? [];
// If any of the namespace strings aren't present in this event, quit.
for (let i = 0; i < namespaceArr.length; i++) {
if (eventNamespace.indexOf(namespaceArr[i]) === -1) return;
}
handler.apply(this, [namespaceArr, handler, ...args]);
};
}
// The size of each image may change either because the browser window was
// resized, or because a tab was shown/hidden (hidden elements report size
// of 0x0). It's OK to over-report sizes because the input pipeline will
// filter out values that haven't changed.
$(window).resize(debounce(500, sendImageSizeFns.regular));
// Need to register callbacks for each Bootstrap 3 class.
const bs3classes = [
"modal",
"dropdown",
"tab",
"tooltip",
"popover",
"collapse",
];
$.each(bs3classes, function (idx, classname) {
$(document.body).on(
"shown.bs." + classname + ".sendImageSize",
"*",
filterEventsByNamespace("bs", sendImageSizeFns.regular)
);
$(document.body).on(
"shown.bs." +
classname +
".sendOutputHiddenState " +
"hidden.bs." +
classname +
".sendOutputHiddenState",
"*",
filterEventsByNamespace("bs", sendOutputHiddenState)
);
});
// This is needed for Bootstrap 2 compatibility and for non-Bootstrap
// related shown/hidden events (like conditionalPanel)
$(document.body).on("shown.sendImageSize", "*", sendImageSizeFns.regular);
$(document.body).on(
"shown.sendOutputHiddenState hidden.sendOutputHiddenState",
"*",
sendOutputHiddenState
);
// Send initial pixel ratio, and update it if it changes
initialValues[".clientdata_pixelratio"] = pixelRatio();
$(window).resize(function () {
inputs.setInput(".clientdata_pixelratio", pixelRatio());
});
// Send initial URL
initialValues[".clientdata_url_protocol"] = window.location.protocol;
initialValues[".clientdata_url_hostname"] = window.location.hostname;
initialValues[".clientdata_url_port"] = window.location.port;
initialValues[".clientdata_url_pathname"] = window.location.pathname;
// Send initial URL search (query string) and update it if it changes
initialValues[".clientdata_url_search"] = window.location.search;
$(window).on("pushstate", function (e) {
inputs.setInput(".clientdata_url_search", window.location.search);
return;
e;
});
$(window).on("popstate", function (e) {
inputs.setInput(".clientdata_url_search", window.location.search);
return;
e;
});
// This is only the initial value of the hash. The hash can change, but
// a reactive version of this isn't sent because watching for changes can
// require polling on some browsers. The JQuery hashchange plugin can be
// used if this capability is important.
initialValues[".clientdata_url_hash_initial"] = window.location.hash;
initialValues[".clientdata_url_hash"] = window.location.hash;
$(window).on("hashchange", function (e) {
inputs.setInput(".clientdata_url_hash", window.location.hash);
return;
e;
});
// The server needs to know what singletons were rendered as part of
// the page loading
const singletonText = (initialValues[".clientdata_singletons"] = $(
'script[type="application/shiny-singletons"]'
).text());
singletonsRegisterNames(singletonText.split(/,/));
const dependencyText = $(
'script[type="application/html-dependencies"]'
).text();
$.each(dependencyText.split(/;/), function (i, depStr) {
const match = /\s*^(.+)\[(.+)\]\s*$/.exec(depStr);
if (match) {
registerDependency(match[1], match[2]);
}
});
// We've collected all the initial values--start the server process!
inputsNoResend.reset(initialValues);
shinyapp.connect(initialValues);
$(document).one("shiny:connected", () => {
initDeferredIframes();
});
$(document).one("shiny:sessioninitialized", () => {
this.initializedPromise.resolve();
});
}
}
// Give any deferred iframes a chance to load.
function initDeferredIframes(): void {
// TODO-barret; This method uses `window.Shiny`. Could be replaced with `fullShinyObj_.shinyapp?.isConnected()`,
// but that would not use `window.Shiny`. Is it a problem???
if (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
// Can not expect error when combining with window available Shiny definition
!window.Shiny ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
// Can not expect error when combining with window available Shiny definition
!window.Shiny.shinyapp ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
// Can not expect error when combining with window available Shiny definition
!window.Shiny.shinyapp.isConnected()
) {
// If somehow we accidentally call this before the server connection is
// established, just ignore the call. At the time of this writing it
// doesn't happen, but it's easy to imagine a later refactoring putting
// us in this situation and it'd be hard to notice with either manual
// testing or automated tests, because the only effect is on HTTP request
// timing. (Update: Actually Aron saw this being called without even
// window.Shiny being defined, but it was hard to repro.)
return;
}
$(".shiny-frame-deferred").each(function (i, el) {
const $el = $(el);
$el.removeClass("shiny-frame-deferred");
// @ts-expect-error; If it is undefined, set using the undefined value
$el.attr("src", $el.attr("data-deferred-src"));
$el.attr("data-deferred-src", null);
}); });
} }
export { windowShiny, setShiny }; export { ShinyClass };
export type { Shiny };

View File

@@ -1,565 +0,0 @@
import $ from "jquery";
import type { Shiny } from ".";
import type { InputPolicy } from "../inputPolicies";
import {
InputBatchSender,
InputDeferDecorator,
InputEventDecorator,
InputNoResendDecorator,
InputRateDecorator,
InputValidateDecorator,
} from "../inputPolicies";
import type { InputPolicyOpts } from "../inputPolicies/inputPolicy";
import { addDefaultInputOpts } from "../inputPolicies/inputValidateDecorator";
import { debounce, Debouncer } from "../time";
import {
getComputedLinkColor,
getStyle,
hasDefinedProperty,
mapValues,
pixelRatio,
} from "../utils";
import type { BindInputsCtx, BindScope } from "./bind";
import { bindAll, unbindAll, _bindAll } from "./bind";
import { setShinyObj } from "./initedMethods";
import { registerDependency } from "./render";
import { sendImageSizeFns } from "./sendImageSize";
import { ShinyApp } from "./shinyapp";
import { registerNames as singletonsRegisterNames } from "./singletons";
// "init_shiny.js"
async function initShiny(windowShiny: Shiny): Promise<void> {
setShinyObj(windowShiny);
const shinyapp = (windowShiny.shinyapp = new ShinyApp());
windowShiny.progressHandlers = shinyapp.progressHandlers;
const inputBatchSender = new InputBatchSender(shinyapp);
const inputsNoResend = new InputNoResendDecorator(inputBatchSender);
const inputsEvent = new InputEventDecorator(inputsNoResend);
const inputsRate = new InputRateDecorator(inputsEvent);
const inputsDefer = new InputDeferDecorator(inputsEvent);
let target: InputPolicy;
if ($('input[type="submit"], button[type="submit"]').length > 0) {
// If there is a submit button on the page, use defer decorator
target = inputsDefer;
$('input[type="submit"], button[type="submit"]').each(function () {
$(this).click(function (event) {
event.preventDefault();
inputsDefer.submit();
});
});
} else {
// By default, use rate decorator
target = inputsRate;
}
const inputs = new InputValidateDecorator(target);
windowShiny.setInputValue = windowShiny.onInputChange = function (
name: string,
value: unknown,
opts: Partial<InputPolicyOpts> = {}
): void {
const newOpts = addDefaultInputOpts(opts);
inputs.setInput(name, value, newOpts);
};
// By default, Shiny deduplicates input value changes; that is, if
// `setInputValue` is called with the same value as the input already
// has, the call is ignored (unless opts.priority = "event"). Calling
// `forgetLastInputValue` tells Shiny that the very next call to
// `setInputValue` for this input id shouldn't be ignored, even if it
// is a dupe of the existing value.
windowShiny.forgetLastInputValue = function (name) {
inputsNoResend.forget(name);
};
// MUST be called after `setShiny()`
const inputBindings = windowShiny.inputBindings;
const outputBindings = windowShiny.outputBindings;
function shinyBindCtx(): BindInputsCtx {
return {
inputs,
inputsRate,
sendOutputHiddenState,
maybeAddThemeObserver,
inputBindings,
outputBindings,
initDeferredIframes,
};
}
windowShiny.bindAll = async function (scope: BindScope) {
await bindAll(shinyBindCtx(), scope);
};
windowShiny.unbindAll = function (scope: BindScope, includeSelf = false) {
unbindAll(shinyBindCtx(), scope, includeSelf);
};
// Calls .initialize() for all of the input objects in all input bindings,
// in the given scope.
function initializeInputs(scope: BindScope = document.documentElement) {
const bindings = inputBindings.getBindings();
// Iterate over all bindings
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i].binding;
const inputObjects = binding.find(scope);
if (inputObjects) {
// Iterate over all input objects for this binding
for (let j = 0; j < inputObjects.length; j++) {
const $inputObjectJ = $(inputObjects[j]);
if (!$inputObjectJ.data("_shiny_initialized")) {
$inputObjectJ.data("_shiny_initialized", true);
binding.initialize(inputObjects[j]);
}
}
}
}
}
windowShiny.initializeInputs = initializeInputs;
function getIdFromEl(el: HTMLElement) {
const $el = $(el);
const bindingAdapter = $el.data("shiny-output-binding");
if (!bindingAdapter) return null;
else return bindingAdapter.getId();
}
// Initialize all input objects in the document, before binding
initializeInputs(document.documentElement);
// The input values returned by _bindAll() each have a structure like this:
// { value: 123, opts: { ... } }
// We want to only keep the value. This is because when the initialValues is
// passed to ShinyApp.connect(), the ShinyApp object stores the
// initialValues object for the duration of the session, and the opts may
// have a reference to the DOM element, which would prevent it from being
// GC'd.
const initialValues = mapValues(
await _bindAll(shinyBindCtx(), document.documentElement),
(x) => x.value
);
// The server needs to know the size of each image and plot output element,
// in case it is auto-sizing
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
function () {
const id = getIdFromEl(this),
rect = this.getBoundingClientRect();
if (rect.width !== 0 || rect.height !== 0) {
initialValues[".clientdata_output_" + id + "_width"] = rect.width;
initialValues[".clientdata_output_" + id + "_height"] = rect.height;
}
}
);
function getComputedBgColor(
el: HTMLElement | null
): string | null | undefined {
if (!el) {
// Top of document, can't recurse further
return null;
}
const bgColor = getStyle(el, "background-color");
if (!bgColor) return bgColor;
const m = bgColor.match(
/^rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/
);
if (bgColor === "transparent" || (m && parseFloat(m[4]) === 0)) {
// No background color on this element. See if it has a background image.
const bgImage = getStyle(el, "background-image");
if (bgImage && bgImage !== "none") {
// Failed to detect background color, since it has a background image
return null;
} else {
// Recurse
return getComputedBgColor(el.parentElement);
}
}
return bgColor;
}
function getComputedFont(el: HTMLElement) {
const fontFamily = getStyle(el, "font-family");
const fontSize = getStyle(el, "font-size");
return {
families: fontFamily?.replace(/"/g, "").split(", "),
size: fontSize,
};
}
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
function () {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const el = this;
const id = getIdFromEl(el);
initialValues[".clientdata_output_" + id + "_bg"] =
getComputedBgColor(el);
initialValues[".clientdata_output_" + id + "_fg"] = getStyle(el, "color");
initialValues[".clientdata_output_" + id + "_accent"] =
getComputedLinkColor(el);
initialValues[".clientdata_output_" + id + "_font"] = getComputedFont(el);
maybeAddThemeObserver(el);
}
);
// Resend computed styles if *an output element's* class or style attribute changes.
// This gives us some level of confidence that getCurrentOutputInfo() will be
// properly invalidated if output container is mutated; but unfortunately,
// we don't have a reasonable way to detect change in *inherited* styles
// (other than session$setCurrentTheme())
// https://github.com/rstudio/shiny/issues/3196
// https://github.com/rstudio/shiny/issues/2998
function maybeAddThemeObserver(el: HTMLElement): void {
if (!window.MutationObserver) {
return; // IE10 and lower
}
const cl = el.classList;
const reportTheme =
cl.contains("shiny-image-output") ||
cl.contains("shiny-plot-output") ||
cl.contains("shiny-report-theme");
if (!reportTheme) {
return;
}
const $el = $(el);
if ($el.data("shiny-theme-observer")) {
return; // i.e., observer is already observing
}
const observerCallback = new Debouncer(null, () => doSendTheme(el), 100);
const observer = new MutationObserver(() => observerCallback.normalCall());
const config = { attributes: true, attributeFilter: ["style", "class"] };
observer.observe(el, config);
$el.data("shiny-theme-observer", observer);
}
function doSendTheme(el: HTMLElement): void {
// Sending theme info on error isn't necessary (it'd add an unnecessary additional round-trip)
if (el.classList.contains("shiny-output-error")) {
return;
}
const id = getIdFromEl(el);
inputs.setInput(".clientdata_output_" + id + "_bg", getComputedBgColor(el));
inputs.setInput(".clientdata_output_" + id + "_fg", getStyle(el, "color"));
inputs.setInput(
".clientdata_output_" + id + "_accent",
getComputedLinkColor(el)
);
inputs.setInput(".clientdata_output_" + id + "_font", getComputedFont(el));
}
function doSendImageSize() {
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
function () {
const id = getIdFromEl(this),
rect = this.getBoundingClientRect();
if (rect.width !== 0 || rect.height !== 0) {
inputs.setInput(".clientdata_output_" + id + "_width", rect.width);
inputs.setInput(".clientdata_output_" + id + "_height", rect.height);
}
}
);
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
function () {
doSendTheme(this);
}
);
$(".shiny-bound-output").each(function () {
const $this = $(this),
binding = $this.data("shiny-output-binding");
$this.trigger({
type: "shiny:visualchange",
// @ts-expect-error; Can not remove info on a established, malformed Event object
visible: !isHidden(this),
binding: binding,
});
binding.onResize();
});
}
sendImageSizeFns.setImageSend(inputBatchSender, doSendImageSize);
// Return true if the object or one of its ancestors in the DOM tree has
// style='display:none'; otherwise return false.
function isHidden(obj: HTMLElement | null): boolean {
// null means we've hit the top of the tree. If width or height is
// non-zero, then we know that no ancestor has display:none.
if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
return false;
} else if (getStyle(obj, "display") === "none") {
return true;
} else {
return isHidden(obj.parentNode as HTMLElement | null);
}
}
let lastKnownVisibleOutputs: { [key: string]: boolean } = {};
// Set initial state of outputs to hidden, if needed
$(".shiny-bound-output").each(function () {
const id = getIdFromEl(this);
if (isHidden(this)) {
initialValues[".clientdata_output_" + id + "_hidden"] = true;
} else {
lastKnownVisibleOutputs[id] = true;
initialValues[".clientdata_output_" + id + "_hidden"] = false;
}
});
// Send update when hidden state changes
function doSendOutputHiddenState() {
const visibleOutputs: { [key: string]: boolean } = {};
$(".shiny-bound-output").each(function () {
const id = getIdFromEl(this);
delete lastKnownVisibleOutputs[id];
// Assume that the object is hidden when width and height are 0
const hidden = isHidden(this),
evt = {
type: "shiny:visualchange",
visible: !hidden,
};
if (hidden) {
inputs.setInput(".clientdata_output_" + id + "_hidden", true);
} else {
visibleOutputs[id] = true;
inputs.setInput(".clientdata_output_" + id + "_hidden", false);
}
const $this = $(this);
// @ts-expect-error; Can not remove info on a established, malformed Event object
evt.binding = $this.data("shiny-output-binding");
// @ts-expect-error; Can not remove info on a established, malformed Event object
$this.trigger(evt);
});
// Anything left in lastKnownVisibleOutputs is orphaned
for (const name in lastKnownVisibleOutputs) {
if (hasDefinedProperty(lastKnownVisibleOutputs, name))
inputs.setInput(".clientdata_output_" + name + "_hidden", true);
}
// Update the visible outputs for next time
lastKnownVisibleOutputs = visibleOutputs;
}
// sendOutputHiddenState gets called each time DOM elements are shown or
// hidden. This can be in the hundreds or thousands of times at startup.
// We'll debounce it, so that we do the actual work once per tick.
const sendOutputHiddenStateDebouncer = new Debouncer(
null,
doSendOutputHiddenState,
0
);
function sendOutputHiddenState() {
sendOutputHiddenStateDebouncer.normalCall();
}
// We need to make sure doSendOutputHiddenState actually gets called before
// the inputBatchSender sends data to the server. The lastChanceCallback
// here does that - if the debouncer has a pending call, flush it.
inputBatchSender.lastChanceCallback.push(function () {
if (sendOutputHiddenStateDebouncer.isPending())
sendOutputHiddenStateDebouncer.immediateCall();
});
// Given a namespace and a handler function, return a function that invokes
// the handler only when e's namespace matches. For example, if the
// namespace is "bs", it would match when e.namespace is "bs" or "bs.tab".
// If the namespace is "bs.tab", it would match for "bs.tab", but not "bs".
function filterEventsByNamespace(
namespace: string,
handler: (...handlerArgs: any[]) => void,
...args: any[]
) {
const namespaceArr = namespace.split(".");
return function (this: HTMLElement, e: JQuery.TriggeredEvent) {
const eventNamespace = e.namespace?.split(".") ?? [];
// If any of the namespace strings aren't present in this event, quit.
for (let i = 0; i < namespaceArr.length; i++) {
if (eventNamespace.indexOf(namespaceArr[i]) === -1) return;
}
handler.apply(this, [namespaceArr, handler, ...args]);
};
}
// The size of each image may change either because the browser window was
// resized, or because a tab was shown/hidden (hidden elements report size
// of 0x0). It's OK to over-report sizes because the input pipeline will
// filter out values that haven't changed.
$(window).resize(debounce(500, sendImageSizeFns.regular));
// Need to register callbacks for each Bootstrap 3 class.
const bs3classes = [
"modal",
"dropdown",
"tab",
"tooltip",
"popover",
"collapse",
];
$.each(bs3classes, function (idx, classname) {
$(document.body).on(
"shown.bs." + classname + ".sendImageSize",
"*",
filterEventsByNamespace("bs", sendImageSizeFns.regular)
);
$(document.body).on(
"shown.bs." +
classname +
".sendOutputHiddenState " +
"hidden.bs." +
classname +
".sendOutputHiddenState",
"*",
filterEventsByNamespace("bs", sendOutputHiddenState)
);
});
// This is needed for Bootstrap 2 compatibility and for non-Bootstrap
// related shown/hidden events (like conditionalPanel)
$(document.body).on("shown.sendImageSize", "*", sendImageSizeFns.regular);
$(document.body).on(
"shown.sendOutputHiddenState hidden.sendOutputHiddenState",
"*",
sendOutputHiddenState
);
// Send initial pixel ratio, and update it if it changes
initialValues[".clientdata_pixelratio"] = pixelRatio();
$(window).resize(function () {
inputs.setInput(".clientdata_pixelratio", pixelRatio());
});
// Send initial URL
initialValues[".clientdata_url_protocol"] = window.location.protocol;
initialValues[".clientdata_url_hostname"] = window.location.hostname;
initialValues[".clientdata_url_port"] = window.location.port;
initialValues[".clientdata_url_pathname"] = window.location.pathname;
// Send initial URL search (query string) and update it if it changes
initialValues[".clientdata_url_search"] = window.location.search;
$(window).on("pushstate", function (e) {
inputs.setInput(".clientdata_url_search", window.location.search);
return;
e;
});
$(window).on("popstate", function (e) {
inputs.setInput(".clientdata_url_search", window.location.search);
return;
e;
});
// This is only the initial value of the hash. The hash can change, but
// a reactive version of this isn't sent because watching for changes can
// require polling on some browsers. The JQuery hashchange plugin can be
// used if this capability is important.
initialValues[".clientdata_url_hash_initial"] = window.location.hash;
initialValues[".clientdata_url_hash"] = window.location.hash;
$(window).on("hashchange", function (e) {
inputs.setInput(".clientdata_url_hash", window.location.hash);
return;
e;
});
// The server needs to know what singletons were rendered as part of
// the page loading
const singletonText = (initialValues[".clientdata_singletons"] = $(
'script[type="application/shiny-singletons"]'
).text());
singletonsRegisterNames(singletonText.split(/,/));
const dependencyText = $(
'script[type="application/html-dependencies"]'
).text();
$.each(dependencyText.split(/;/), function (i, depStr) {
const match = /\s*^(.+)\[(.+)\]\s*$/.exec(depStr);
if (match) {
registerDependency(match[1], match[2]);
}
});
// We've collected all the initial values--start the server process!
inputsNoResend.reset(initialValues);
shinyapp.connect(initialValues);
$(document).one("shiny:connected", function () {
initDeferredIframes();
});
// window.console.log("Shiny version: ", windowShiny.version);
} // function initShiny()
// Give any deferred iframes a chance to load.
function initDeferredIframes(): void {
// TODO-barret; This method uses `window.Shiny`. Could be replaced with `fullShinyObj_.shinyapp?.isConnected()`,
// but that would not use `window.Shiny`. Is it a problem???
if (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
// Can not expect error when combining with window available Shiny definition
!window.Shiny ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
// Can not expect error when combining with window available Shiny definition
!window.Shiny.shinyapp ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
// Can not expect error when combining with window available Shiny definition
!window.Shiny.shinyapp.isConnected()
) {
// If somehow we accidentally call this before the server connection is
// established, just ignore the call. At the time of this writing it
// doesn't happen, but it's easy to imagine a later refactoring putting
// us in this situation and it'd be hard to notice with either manual
// testing or automated tests, because the only effect is on HTTP request
// timing. (Update: Actually Aron saw this being called without even
// window.Shiny being defined, but it was hard to repro.)
return;
}
$(".shiny-frame-deferred").each(function (i, el) {
const $el = $(el);
$el.removeClass("shiny-frame-deferred");
// @ts-expect-error; If it is undefined, set using the undefined value
$el.attr("src", $el.attr("data-deferred-src"));
$el.attr("data-deferred-src", null);
});
}
export { initShiny };

View File

@@ -1,4 +1,4 @@
import type { Shiny } from "."; import type { ShinyClass } from ".";
import type { FileInputBinding } from "../bindings/input/fileinput"; import type { FileInputBinding } from "../bindings/input/fileinput";
import type { OutputBindingAdapter } from "../bindings/outputAdapter"; import type { OutputBindingAdapter } from "../bindings/outputAdapter";
import type { EventPriority } from "../inputPolicies"; import type { EventPriority } from "../inputPolicies";
@@ -10,7 +10,7 @@ let fullShinyObj: FullShinyDef;
// TODO-future; It would be nice to have a way to export this type value instead of / in addition to `Shiny` // TODO-future; It would be nice to have a way to export this type value instead of / in addition to `Shiny`
type FullShinyDef = Required< type FullShinyDef = Required<
Pick< Pick<
Shiny, ShinyClass,
| "bindAll" | "bindAll"
| "forgetLastInputValue" | "forgetLastInputValue"
| "initializeInputs" | "initializeInputs"
@@ -21,9 +21,9 @@ type FullShinyDef = Required<
| "user" | "user"
> >
> & > &
Shiny; ShinyClass;
function setShinyObj(shiny: Shiny): void { function setShinyObj(shiny: ShinyClass): void {
fullShinyObj = shiny as FullShinyDef; fullShinyObj = shiny as FullShinyDef;
} }

View File

@@ -109,6 +109,9 @@ function addCustomMessageHandler(type: string, handler: Handler): void {
//// End message handler variables //// End message handler variables
/**
* The ShinyApp class handles the communication with the Shiny Server.
*/
class ShinyApp { class ShinyApp {
$socket: ShinyWebSocket | null = null; $socket: ShinyWebSocket | null = null;
@@ -1610,4 +1613,4 @@ class ShinyApp {
} }
export { ShinyApp, addCustomMessageHandler }; export { ShinyApp, addCustomMessageHandler };
export type { Handler, ErrorsMessageValue }; export type { Handler, ErrorsMessageValue, ShinyWebSocket };

View File

@@ -0,0 +1,46 @@
// A shim for Promise.withResolvers. Once browser support is widespread, we can
// remove this.
export function promiseWithResolvers<T>(): {
promise: Promise<T>;
resolve: (value: PromiseLike<T> | T) => void;
reject: (reason?: any) => void;
} {
let resolve: (value: PromiseLike<T> | T) => void;
let reject: (reason?: any) => void;
const promise = new Promise(
(res: (value: PromiseLike<T> | T) => void, rej: (reason?: any) => void) => {
resolve = res;
reject = rej;
}
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return { promise, resolve: resolve!, reject: reject! };
}
export interface InitStatusPromise<T> extends Promise<T> {
promise: Promise<T>;
resolve(x: T): void;
resolved(): boolean;
}
export function createInitStatus<T>(): InitStatusPromise<T> {
const { promise, resolve } = promiseWithResolvers<T>();
// eslint-disable-next-line @typescript-eslint/naming-convention
let _resolved = false;
return {
promise,
resolve(x: T) {
_resolved = true;
resolve(x);
},
then: promise.then.bind(promise),
catch: promise.catch.bind(promise),
finally: promise.finally.bind(promise),
[Symbol.toStringTag]: "InitStatus",
resolved() {
return _resolved;
},
};
}

View File

@@ -1,12 +0,0 @@
import type { Shiny } from "../shiny";
function windowShiny(): Shiny {
// Use `any` type as we know what we are doing is _dangerous_
// Immediately init shiny on the window
if (!(window as any)["Shiny"]) {
(window as any)["Shiny"] = {};
}
return (window as any)["Shiny"];
}
export { windowShiny };

View File

@@ -1,8 +1,6 @@
import type { Shiny as RStudioShiny } from "../src/shiny/index"; import type { ShinyClass } from "../src/shiny/index";
declare global { declare global {
const Shiny: RStudioShiny;
interface Window { interface Window {
Shiny: RStudioShiny; Shiny: ShinyClass;
} }
type Shiny = RStudioShiny;
} }

View File

@@ -1,9 +1,8 @@
import { BindingRegistry } from "../registry"; import { BindingRegistry } from "../registry";
import { InputBinding } from "./inputBinding"; import { InputBinding } from "./inputBinding";
import { FileInputBinding } from "./fileinput"; import { FileInputBinding } from "./fileinput";
type InitInputBindings = { declare function initInputBindings(): {
inputBindings: BindingRegistry<InputBinding>; inputBindings: BindingRegistry<InputBinding>;
fileInputBinding: FileInputBinding; fileInputBinding: FileInputBinding;
}; };
declare function initInputBindings(): InitInputBindings;
export { initInputBindings, InputBinding }; export { initInputBindings, InputBinding };

View File

@@ -1 +1 @@
export {}; export { Shiny, type ShinyClass } from "./initialize";

View File

@@ -1,2 +1,4 @@
import { ShinyClass } from "../shiny";
declare let Shiny: ShinyClass;
declare function init(): void; declare function init(): void;
export { init }; export { init, Shiny, type ShinyClass };

View File

@@ -1,22 +1,21 @@
import { InputBinding, OutputBinding } from "../bindings"; import { InputBinding, OutputBinding } from "../bindings";
import { initInputBindings } from "../bindings/input"; import type { BindingRegistry } from "../bindings/registry";
import { initOutputBindings } from "../bindings/output";
import { resetBrush } from "../imageutils/resetBrush"; import { resetBrush } from "../imageutils/resetBrush";
import { $escape, compareVersion } from "../utils"; import { $escape, compareVersion } from "../utils";
import { type InitStatusPromise } from "../utils/promise";
import type { shinyBindAll, shinyForgetLastInputValue, shinyInitializeInputs, shinySetInputValue, shinyUnbindAll } from "./initedMethods"; import type { shinyBindAll, shinyForgetLastInputValue, shinyInitializeInputs, shinySetInputValue, shinyUnbindAll } from "./initedMethods";
import { removeModal, showModal } from "./modal"; import { removeModal, showModal } from "./modal";
import { removeNotification, showNotification } from "./notifications"; import { removeNotification, showNotification } from "./notifications";
import { hideReconnectDialog, showReconnectDialog } from "./reconnectDialog"; import { hideReconnectDialog, showReconnectDialog } from "./reconnectDialog";
import { renderContent, renderContentAsync, renderDependencies, renderDependenciesAsync, renderHtml, renderHtmlAsync } from "./render"; import { renderContent, renderContentAsync, renderDependencies, renderDependenciesAsync, renderHtml, renderHtmlAsync } from "./render";
import type { Handler, ShinyApp } from "./shinyapp"; import { addCustomMessageHandler, ShinyApp, type Handler } from "./shinyapp";
import { addCustomMessageHandler } from "./shinyapp"; declare class ShinyClass {
interface Shiny {
version: string; version: string;
$escape: typeof $escape; $escape: typeof $escape;
compareVersion: typeof compareVersion; compareVersion: typeof compareVersion;
inputBindings: ReturnType<typeof initInputBindings>["inputBindings"]; inputBindings: BindingRegistry<InputBinding>;
InputBinding: typeof InputBinding; InputBinding: typeof InputBinding;
outputBindings: ReturnType<typeof initOutputBindings>["outputBindings"]; outputBindings: BindingRegistry<OutputBinding>;
OutputBinding: typeof OutputBinding; OutputBinding: typeof OutputBinding;
resetBrush: typeof resetBrush; resetBrush: typeof resetBrush;
notifications: { notifications: {
@@ -27,7 +26,6 @@ interface Shiny {
show: typeof showModal; show: typeof showModal;
remove: typeof removeModal; remove: typeof removeModal;
}; };
createSocket?: () => WebSocket;
showReconnectDialog: typeof showReconnectDialog; showReconnectDialog: typeof showReconnectDialog;
hideReconnectDialog: typeof hideReconnectDialog; hideReconnectDialog: typeof hideReconnectDialog;
renderDependenciesAsync: typeof renderDependenciesAsync; renderDependenciesAsync: typeof renderDependenciesAsync;
@@ -36,9 +34,10 @@ interface Shiny {
renderContent: typeof renderContent; renderContent: typeof renderContent;
renderHtmlAsync: typeof renderHtmlAsync; renderHtmlAsync: typeof renderHtmlAsync;
renderHtml: typeof renderHtml; renderHtml: typeof renderHtml;
user: string;
progressHandlers?: ShinyApp["progressHandlers"];
addCustomMessageHandler: typeof addCustomMessageHandler; addCustomMessageHandler: typeof addCustomMessageHandler;
createSocket?: () => WebSocket;
user?: string;
progressHandlers?: ShinyApp["progressHandlers"];
shinyapp?: ShinyApp; shinyapp?: ShinyApp;
setInputValue?: typeof shinySetInputValue; setInputValue?: typeof shinySetInputValue;
onInputChange?: typeof shinySetInputValue; onInputChange?: typeof shinySetInputValue;
@@ -46,16 +45,16 @@ interface Shiny {
bindAll?: typeof shinyBindAll; bindAll?: typeof shinyBindAll;
unbindAll?: typeof shinyUnbindAll; unbindAll?: typeof shinyUnbindAll;
initializeInputs?: typeof shinyInitializeInputs; initializeInputs?: typeof shinyInitializeInputs;
initializedPromise: InitStatusPromise<void>;
oncustommessage?: Handler; oncustommessage?: Handler;
constructor();
/** /**
* Method to check if Shiny is running in development mode. By packaging as a * Method to check if Shiny is running in development mode. By packaging as a
* method, we can we can avoid needing to look for the `__SHINY_DEV_MODE__` * method, we can we can avoid needing to look for the `__SHINY_DEV_MODE__`
* variable in the global scope. * variable in the global scope.
* @returns `true` if Shiny is running in development mode, `false` otherwise. * @returns `true` if Shiny is running in development mode, `false` otherwise.
*/ */
inDevMode: () => boolean; inDevMode(): boolean;
initialize(): Promise<void>;
} }
declare let windowShiny: Shiny; export { ShinyClass };
declare function setShiny(windowShiny_: Shiny): void;
export { windowShiny, setShiny };
export type { Shiny };

View File

@@ -1,3 +0,0 @@
import type { Shiny } from ".";
declare function initShiny(windowShiny: Shiny): Promise<void>;
export { initShiny };

View File

@@ -1,10 +1,10 @@
import type { Shiny } from "."; import type { ShinyClass } from ".";
import type { FileInputBinding } from "../bindings/input/fileinput"; import type { FileInputBinding } from "../bindings/input/fileinput";
import type { OutputBindingAdapter } from "../bindings/outputAdapter"; import type { OutputBindingAdapter } from "../bindings/outputAdapter";
import type { EventPriority } from "../inputPolicies"; import type { EventPriority } from "../inputPolicies";
import type { BindScope } from "./bind"; import type { BindScope } from "./bind";
import type { Handler, ShinyApp } from "./shinyapp"; import type { Handler, ShinyApp } from "./shinyapp";
declare function setShinyObj(shiny: Shiny): void; declare function setShinyObj(shiny: ShinyClass): void;
declare function shinySetInputValue(name: string, value: unknown, opts?: { declare function shinySetInputValue(name: string, value: unknown, opts?: {
priority?: EventPriority; priority?: EventPriority;
}): void; }): void;

View File

@@ -19,6 +19,9 @@ type InputValues = {
}; };
type MessageValue = Parameters<WebSocket["send"]>[0]; type MessageValue = Parameters<WebSocket["send"]>[0];
declare function addCustomMessageHandler(type: string, handler: Handler): void; declare function addCustomMessageHandler(type: string, handler: Handler): void;
/**
* The ShinyApp class handles the communication with the Shiny Server.
*/
declare class ShinyApp { declare class ShinyApp {
$socket: ShinyWebSocket | null; $socket: ShinyWebSocket | null;
taskQueue: AsyncQueue<() => Promise<void> | void>; taskQueue: AsyncQueue<() => Promise<void> | void>;
@@ -104,4 +107,4 @@ declare class ShinyApp {
}): string; }): string;
} }
export { ShinyApp, addCustomMessageHandler }; export { ShinyApp, addCustomMessageHandler };
export type { Handler, ErrorsMessageValue }; export type { Handler, ErrorsMessageValue, ShinyWebSocket };

11
srcts/types/src/utils/promise.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
export declare function promiseWithResolvers<T>(): {
promise: Promise<T>;
resolve: (value: PromiseLike<T> | T) => void;
reject: (reason?: any) => void;
};
export interface InitStatusPromise<T> extends Promise<T> {
promise: Promise<T>;
resolve(x: T): void;
resolved(): boolean;
}
export declare function createInitStatus<T>(): InitStatusPromise<T>;

View File

@@ -1,3 +0,0 @@
import type { Shiny } from "../shiny";
declare function windowShiny(): Shiny;
export { windowShiny };