Compare commits

...

6 Commits

12 changed files with 1807 additions and 813 deletions

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

@@ -1,4 +1,5 @@
import { mergeSort } from "../utils";
import { Callbacks } from "../utils/callbacks";
interface BindingBase {
name: string;
@@ -14,6 +15,7 @@ class BindingRegistry<Binding extends BindingBase> {
name: string;
bindings: Array<BindingObj<Binding>> = [];
bindingNames: { [key: string]: BindingObj<Binding> } = {};
registerCallbacks: Callbacks = new Callbacks();
register(binding: Binding, bindingName: string, priority = 0): void {
const bindingObj = { binding, priority };
@@ -23,6 +25,12 @@ class BindingRegistry<Binding extends BindingBase> {
this.bindingNames[bindingName] = bindingObj;
binding.name = bindingName;
}
this.registerCallbacks.invoke();
}
onRegister(fn: () => void, once = true): void {
this.registerCallbacks.register(fn, once);
}
setPriority(bindingName: string, priority: number): void {

View File

@@ -21,7 +21,7 @@ import {
import { bindAll, unbindAll, _bindAll } from "./bind";
import type { BindInputsCtx, BindScope } from "./bind";
import { setShinyObj } from "./initedMethods";
import { registerDependency } from "./render";
import { registerDependency, renderHtml } from "./render";
import { sendImageSizeFns } from "./sendImageSize";
import { ShinyApp } from "./shinyapp";
import { registerNames as singletonsRegisterNames } from "./singletons";
@@ -150,6 +150,19 @@ function initShiny(windowShiny: Shiny): void {
(x) => x.value
);
// When future bindings are registered via dynamic UI, check to see if renderHtml()
// is currently executing. If it's not, it's likely that the binding registration
// is occurring a tick after renderHtml()/renderContent(), in which case we need
// to make sure the new bindings get a chance to bind to the DOM. (#3635)
const maybeBindOnRegister = debounce(0, () => {
if (!renderHtml.isExecuting()) {
windowShiny.bindAll(document.documentElement);
}
});
inputBindings.onRegister(maybeBindOnRegister, false);
outputBindings.onRegister(maybeBindOnRegister, false);
// 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(

View File

@@ -72,10 +72,20 @@ function renderHtml(
dependencies: HtmlDep[],
where: WherePosition = "replace"
): ReturnType<typeof singletonsRenderHtml> {
renderDependencies(dependencies);
return singletonsRenderHtml(html, el, where);
renderHtml._renderCount++;
try {
renderDependencies(dependencies);
return singletonsRenderHtml(html, el, where);
} finally {
renderHtml._renderCount--;
}
}
renderHtml._renderCount = 0;
renderHtml.isExecuting = function () {
return renderHtml._renderCount > 0;
};
type HtmlDepVersion = string;
type MetaItem = {
@@ -199,6 +209,9 @@ function renderDependency(dep_: HtmlDep) {
$head.append(stylesheetLinks);
}
const scriptPromises: Array<Promise<any>> = [];
const scriptElements: HTMLScriptElement[] = [];
dep.script.forEach((x) => {
const script = document.createElement("script");
@@ -210,9 +223,23 @@ function renderDependency(dep_: HtmlDep) {
script.setAttribute(attr, val ? val : "");
});
$head.append(script);
const p = new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
script.onload = (e: Event) => {
resolve(null);
};
});
scriptPromises.push(p);
scriptElements.push(script);
});
// Append the script elements all at once, so that we're sure they'll load in
// order. (We didn't append them individually in the `forEach()` above,
// because we're not sure that the browser will load them in order if done
// that way.)
document.head.append(...scriptElements);
dep.attachment.forEach((x) => {
const link = $("<link rel='attachment'>")
.attr("id", dep.name + "-" + x.key + "-attachment")
@@ -221,12 +248,22 @@ function renderDependency(dep_: HtmlDep) {
$head.append(link);
});
if (dep.head) {
const $newHead = $("<head></head>");
Promise.allSettled(scriptPromises).then(() => {
// After the scripts are all loaded, insert any head content. This may
// contain <script> tags with inline content, which we want to execute after
// the script elements above, because the code here may depend on them.
if (dep.head) {
const $newHead = $("<head></head>");
$newHead.html(dep.head);
$head.append($newHead.children());
}
// Bind all
shinyInitializeInputs(document.body);
shinyBindAll(document.body);
});
$newHead.html(dep.head);
$head.append($newHead.children());
}
return true;
}

View File

@@ -0,0 +1,45 @@
type Cb = {
once: boolean;
fn: () => void;
};
type Cbs = {
[key: string]: Cb;
};
class Callbacks {
callbacks: Cbs = {};
id = 0;
register(fn: () => void, once = true): () => void {
this.id += 1;
const id = this.id;
this.callbacks[id] = { fn, once };
return () => {
delete this.callbacks[id];
};
}
invoke(): void {
for (const id in this.callbacks) {
const cb = this.callbacks[id];
try {
cb.fn();
} finally {
if (cb.once) delete this.callbacks[id];
}
}
}
clear(): void {
this.callbacks = {};
}
count(): number {
return Object.keys(this.callbacks).length;
}
}
export { Callbacks };

View File

@@ -1,3 +1,4 @@
import { Callbacks } from "../utils/callbacks";
interface BindingBase {
name: string;
}
@@ -12,7 +13,9 @@ declare class BindingRegistry<Binding extends BindingBase> {
bindingNames: {
[key: string]: BindingObj<Binding>;
};
registerCallbacks: Callbacks;
register(binding: Binding, bindingName: string, priority?: number): void;
onRegister(fn: () => void, once?: boolean): void;
setPriority(bindingName: string, priority: number): void;
getPriority(bindingName: string): number | false;
getBindings(): Array<BindingObj<Binding>>;

View File

@@ -7,6 +7,10 @@ declare function renderContent(el: BindScope, content: string | {
deps?: HtmlDep[];
} | null, where?: WherePosition): void;
declare function renderHtml(html: string, el: BindScope, dependencies: HtmlDep[], where?: WherePosition): ReturnType<typeof singletonsRenderHtml>;
declare namespace renderHtml {
var _renderCount: number;
var isExecuting: () => boolean;
}
declare type HtmlDepVersion = string;
declare type MetaItem = {
name: string;

16
srcts/types/src/utils/callbacks.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
declare type Cb = {
once: boolean;
fn: () => void;
};
declare type Cbs = {
[key: string]: Cb;
};
declare class Callbacks {
callbacks: Cbs;
id: number;
register(fn: () => void, once?: boolean): () => void;
invoke(): void;
clear(): void;
count(): number;
}
export { Callbacks };

View File

@@ -1,12 +1,13 @@
{
"declaration": true,
"compilerOptions": {
"target": "ES5",
"target": "es2020",
"isolatedModules": true,
"esModuleInterop": true,
"declaration": true,
"declarationDir": "./srcts/types",
"emitDeclarationOnly": true,
"moduleResolution": "node",
// Can not use `types: []` to disable injecting NodeJS types. More types are
// needed than just the DOM's `window.setTimeout`
// "types": [],