mirror of
https://github.com/rstudio/shiny.git
synced 2026-04-29 03:00:45 -04:00
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:
@@ -35,10 +35,6 @@ rules:
|
|||||||
|
|
||||||
default-case:
|
default-case:
|
||||||
- error
|
- error
|
||||||
indent:
|
|
||||||
- error
|
|
||||||
- 2
|
|
||||||
- SwitchCase: 1
|
|
||||||
linebreak-style:
|
linebreak-style:
|
||||||
- error
|
- error
|
||||||
- unix
|
- unix
|
||||||
|
|||||||
4
NEWS.md
4
NEWS.md
@@ -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
2
inst/www/shared/shiny.min.js
vendored
2
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
import { init } from "./initialize";
|
import { init } from "./initialize";
|
||||||
|
export { Shiny, type ShinyClass } from "./initialize";
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
46
srcts/src/utils/promise.ts
Normal file
46
srcts/src/utils/promise.ts
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 };
|
|
||||||
6
srcts/types/extras/globalShiny.d.ts
vendored
6
srcts/types/extras/globalShiny.d.ts
vendored
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
3
srcts/types/src/bindings/input/index.d.ts
vendored
3
srcts/types/src/bindings/input/index.d.ts
vendored
@@ -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 };
|
||||||
|
|||||||
2
srcts/types/src/index.d.ts
vendored
2
srcts/types/src/index.d.ts
vendored
@@ -1 +1 @@
|
|||||||
export {};
|
export { Shiny, type ShinyClass } from "./initialize";
|
||||||
|
|||||||
4
srcts/types/src/initialize/index.d.ts
vendored
4
srcts/types/src/initialize/index.d.ts
vendored
@@ -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 };
|
||||||
|
|||||||
29
srcts/types/src/shiny/index.d.ts
vendored
29
srcts/types/src/shiny/index.d.ts
vendored
@@ -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 };
|
|
||||||
|
|||||||
3
srcts/types/src/shiny/init.d.ts
vendored
3
srcts/types/src/shiny/init.d.ts
vendored
@@ -1,3 +0,0 @@
|
|||||||
import type { Shiny } from ".";
|
|
||||||
declare function initShiny(windowShiny: Shiny): Promise<void>;
|
|
||||||
export { initShiny };
|
|
||||||
4
srcts/types/src/shiny/initedMethods.d.ts
vendored
4
srcts/types/src/shiny/initedMethods.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
5
srcts/types/src/shiny/shinyapp.d.ts
vendored
5
srcts/types/src/shiny/shinyapp.d.ts
vendored
@@ -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
11
srcts/types/src/utils/promise.d.ts
vendored
Normal 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>;
|
||||||
3
srcts/types/src/window/libraries.d.ts
vendored
3
srcts/types/src/window/libraries.d.ts
vendored
@@ -1,3 +0,0 @@
|
|||||||
import type { Shiny } from "../shiny";
|
|
||||||
declare function windowShiny(): Shiny;
|
|
||||||
export { windowShiny };
|
|
||||||
Reference in New Issue
Block a user