Compare commits

...

21 Commits

Author SHA1 Message Date
Garrick Aden-Buie
10c2398990 Add jsdoc strings to shinyEvents.ts 2023-05-01 09:54:09 -04:00
Garrick Aden-Buie
064da3a883 yarn build 2023-04-27 10:05:33 -04:00
Garrick Aden-Buie
eddb0d9f22 Add more context in comments 2023-04-27 10:05:33 -04:00
Garrick Aden-Buie
4648423c54 Use more descriptive names for events, not evt 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
0adeb4bd36 DRY properties and type definitions
* Use getters to expose data from `this.event` as if they were top-level
* Refer to the interfaces directly rather than repeating type definitions
2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
ce4961f5ef EventBase.triggerOn() now accepts jquery html elements 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
4f50796a3d import $ from jquery 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
08ced6badb Use @babel/plugin-transform-typescript
It was already used by preset-typescript, but we need to use the
plugin so that the transpilation happens at the right time, in
particular because we are using `declare` within classes.
2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
a66ef6de01 Implement EventMessage for shiny:message 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
b5c6561dc9 Implement EventError for shiny:error events 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
2740be168a Implement EventValue for shiny:value 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
922d16d387 Implement EventUpdateInput for shiny:updateinput 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
651b768310 Implement one lower-level event class EventBase and use event rather than evt
It turns out the props on EventCommon aren't used everywhere
2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
63427a7178 Go back to using EvtFn() to extend jquery event handler 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
54fa8570a8 Use EventInputChanged in InputEventDecorator 2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
60f074daac Make the EventCommon class work more like an Event 2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
e2d19b9392 Use EventInputChanged() to emit file upload input event
We now do this in place rather than as a separate function,
i.e. we replace `triggerFileInputChanged()` with our improved
abstraction.
2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
0e2d040c54 Implement EventCommon and EventInputChanged classes
These classes create the custom Shiny events and help us extend JQuery's event handlers for these event types.
2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
8d12ae7351 Remove value, add el to ShinyEventInputChanged
Also mark `priority` as optional since it was optionally provided in practice
2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
90c052341f Allow any-typed arguments in exported functions 2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
01fd8fac64 Remove el from ShinyEventCommon, value is any 2023-04-27 10:03:57 -04:00
16 changed files with 3100 additions and 1842 deletions

View File

@@ -27,6 +27,7 @@ rules:
- off
"@typescript-eslint/explicit-module-boundary-types":
- error
- allowArgumentsExplicitlyTypedAsAny: true
default-case:
- error

View File

@@ -1,4 +1,12 @@
{
"plugins": [
[
"@babel/plugin-transform-typescript",
{
"allowDeclareFields": true
}
]
],
"presets": [
"@babel/preset-typescript",
[
@@ -9,7 +17,7 @@
}
]
],
"ignore":[
"ignore": [
"node_modules/core-js"
]
}

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

@@ -19,6 +19,7 @@
"yarn": ">= 1.22"
},
"dependencies": {
"@babel/plugin-transform-typescript": "^7.21.3",
"@types/bootstrap": "3.4.0",
"@types/bootstrap-datepicker": "0.0.14",
"@types/datatables.net": "^1.10.19",

View File

@@ -1,26 +0,0 @@
import $ from "jquery";
import type { FileInputBinding } from "../bindings/input/fileinput";
import type { ShinyEventInputChanged } from "./shinyEvents";
function triggerFileInputChanged(
name: string,
value: unknown,
binding: FileInputBinding,
el: HTMLElement,
inputType: string,
onEl: typeof document
): ShinyEventInputChanged {
const evt = $.Event("shiny:inputchanged") as ShinyEventInputChanged;
evt.name = name;
evt.value = value;
evt.binding = binding;
evt.el = el;
evt.inputType = inputType;
$(onEl).trigger(evt);
return evt;
}
export { triggerFileInputChanged };

View File

@@ -54,3 +54,5 @@ declare global {
): this;
}
}
export type { EvtFn };

View File

@@ -2,35 +2,589 @@ import type { InputBinding } from "../bindings/input/inputBinding";
import type { OutputBindingAdapter } from "../bindings/outputAdapter";
import type { EventPriority } from "../inputPolicies/inputPolicy";
import type { ErrorsMessageValue } from "../shiny/shinyapp";
import type { EvtFn } from "./jQueryEvents";
import $ from "jquery";
/**
* Shiny Event Base
*
* This class implements a common interface for all Shiny events, and provides a
* layer of abstraction between the Shiny event and the underlying jQuery event
* object. We use a new class, rather than extending JQuery.Event, because
* JQuery.Event is an old function-style class. Each Event class has a
* corresponding ShinyEvent interface that describes the event object that is
* emitted. At the end of this file, we extend JQuery's `on()` method to
* associate the ShinyEvent interfaces with their corresponding event string.
*
* @class EventBase
* @typedef {EventBase}
*/
class EventBase {
/**
* The underlying jQuery event object wrapped by `EventBase`.
* @type {JQuery.Event}
*/
event: JQuery.Event;
/**
* Creates an instance of EventBase.
*
* @constructor
* @param {string} type - The event type.
*/
constructor(type: string) {
this.event = $.Event(type);
}
/**
* Trigger the event on an element or the document.
*
* @example
* // Instead of this...
* el.trigger(shinyEvent);
* // ...do this
* shinyEvent.triggerOn(el);
*
* @param {(HTMLElement | JQuery<HTMLElement> | typeof document | null)} el -
* The element to trigger the event on, or `null` to trigger on `document`.
*/
triggerOn(
el: HTMLElement | JQuery<HTMLElement> | typeof document | null
): void {
$(el || window.document).trigger(this.event);
}
/**
* Proxy for `event.preventDefault()`.
*
* @returns {boolean} `true` if the default action was prevented, `false`
* otherwise.
*/
isDefaultPrevented(): boolean {
return this.event.isDefaultPrevented();
}
}
/**
* A common interface for most Shiny events.
*
* @interface ShinyEventCommon
* @typedef {ShinyEventCommon}
* @extends {JQuery.Event}
*/
interface ShinyEventCommon extends JQuery.Event {
/**
* The event name.
* @type {string}
*/
name: string;
value: unknown;
el: HTMLElement | null;
/**
* Event value containing arbitrary event data.
* @type {*}
*/
value: any;
}
/**
* Create a common Shiny event.
*
* @class EventCommon
* @typedef {EventCommon}
* @extends {EventBase}
*/
class EventCommon extends EventBase {
/**
* The actual event object.
* @type {ShinyEventCommon}
*/
declare event: ShinyEventCommon;
/**
* Creates an instance of EventCommon.
*
* @constructor
* @param {ShinyEventCommon["type"]} type - The Shiny custom event type, e.g.
* `shiny:value`.
* @param {ShinyEventCommon["name"]} name - The event name.
* @param {ShinyEventCommon["value"]} value - The event value, or arbitrary
* data included with the event.
*/
constructor(
type: ShinyEventCommon["type"],
name: ShinyEventCommon["name"],
value: ShinyEventCommon["value"]
) {
super(type);
this.event.name = name;
this.event.value = value;
}
/**
* Get the event name.
* @readonly
* @type {ShinyEventCommon["name"]}
*/
get name(): ShinyEventCommon["name"] {
return this.event.name;
}
/**
* Get the event value.
* @readonly
* @type {ShinyEventCommon["value"]}
*/
get value(): ShinyEventCommon["value"] {
return this.event.value;
}
}
/**
* An interface for the `shiny:inputchanged` event.
*
* @interface ShinyEventInputChanged
* @typedef {ShinyEventInputChanged}
* @extends {ShinyEventCommon}
*/
interface ShinyEventInputChanged extends ShinyEventCommon {
value: unknown;
/**
* The input element whose value has changed.
* @type {(HTMLElement | null)}
*/
el: HTMLElement | null;
/**
* The input binding for the changed input.
* @type {(InputBinding | null)}
*/
binding: InputBinding | null;
/**
* The input type.
* @type {string}
*/
inputType: string;
priority: EventPriority;
/**
* The input event priority.
* @type {?EventPriority}
*/
priority?: EventPriority;
}
/**
* Create a custom `shiny:inputchanged` event as an instance of
* EventInputChanged.
*
* @class EventInputChanged
* @typedef {EventInputChanged}
* @extends {EventCommon}
*/
class EventInputChanged extends EventCommon {
/**
* The `ShinyEventInputChanged` event object.
* @type {ShinyEventInputChanged}
*/
declare event: ShinyEventInputChanged;
/**
* Creates an instance of EventInputChanged.
*
* @constructor
* @param {{
name: ShinyEventInputChanged["name"];
value: ShinyEventInputChanged["value"];
el: ShinyEventInputChanged["el"];
binding: ShinyEventInputChanged["binding"];
inputType: ShinyEventInputChanged["inputType"];
priority?: ShinyEventInputChanged["priority"];
}} {
name,
value,
el,
binding,
inputType,
priority,
}
*/
constructor({
name,
value,
el,
binding,
inputType,
priority,
}: {
name: ShinyEventInputChanged["name"];
value: ShinyEventInputChanged["value"];
el: ShinyEventInputChanged["el"];
binding: ShinyEventInputChanged["binding"];
inputType: ShinyEventInputChanged["inputType"];
priority?: ShinyEventInputChanged["priority"];
}) {
super("shiny:inputchanged", name, value);
this.event.el = el;
this.event.binding = binding;
this.event.inputType = inputType;
if (priority) {
this.event.priority = priority;
}
}
/**
* Get the input element whose value has changed.
* @readonly
* @type {ShinyEventInputChanged["el"]}
*/
get el(): ShinyEventInputChanged["el"] {
return this.event.el;
}
/**
* Get the input binding for the changed input.
* @readonly
* @type {ShinyEventInputChanged["binding"]}
*/
get binding(): ShinyEventInputChanged["binding"] {
return this.event.binding;
}
/**
* Get the input type.
* @readonly
* @type {ShinyEventInputChanged["inputType"]}
*/
get inputType(): ShinyEventInputChanged["inputType"] {
return this.event.inputType;
}
/**
* Get the input event priority.
* @readonly
* @type {ShinyEventInputChanged["priority"]}
*/
get priority(): ShinyEventInputChanged["priority"] {
return this.event.priority;
}
}
/**
* A interface for the custom `shiny:updateinput` event.
*
* @interface ShinyEventUpdateInput
* @typedef {ShinyEventUpdateInput}
* @extends {ShinyEventCommon}
*/
interface ShinyEventUpdateInput extends ShinyEventCommon {
message: unknown;
/**
* Arbitrary message data, typically sent from the server, to be processed by
* the `receiveMessage` method of the input binding.
* @type {?*}
*/
message?: any;
/**
* The input binding for the input.
* @type {InputBinding}
*/
binding: InputBinding;
}
/**
* Create a shiny custom `shiny:updateinput` event as an instance of
* EventUpdateInput. This event carries message data from the server, sent via
* `session$sendInputMessage()`, to the input binding's `receiveMessage` method.
*
* @class EventUpdateInput
* @typedef {EventUpdateInput}
* @extends {EventBase}
*/
class EventUpdateInput extends EventBase {
/**
* The `ShinyEventUpdateInput` event object.
* @type {ShinyEventUpdateInput}
*/
declare event: ShinyEventUpdateInput;
/**
* Creates an instance of EventUpdateInput.
*
* @constructor
* @param {{
message?: ShinyEventUpdateInput["message"];
binding: ShinyEventUpdateInput["binding"];
}} {
message,
binding,
}
*/
constructor({
message,
binding,
}: {
message?: ShinyEventUpdateInput["message"];
binding: ShinyEventUpdateInput["binding"];
}) {
super("shiny:updateinput");
if (message) {
this.event.message = message;
}
this.event.binding = binding;
}
/**
* Get the `shiny:updateinput` message data.
* @readonly
* @type {ShinyEventUpdateInput["message"]}
*/
get message(): ShinyEventUpdateInput["message"] {
return this.event.message;
}
/**
* Get the input binding for the input.
* @readonly
* @type {ShinyEventUpdateInput["binding"]}
*/
get binding(): ShinyEventUpdateInput["binding"] {
return this.event.binding;
}
}
/**
* A interface for the custom `shiny:value` event.
*
* @interface ShinyEventValue
* @typedef {ShinyEventValue}
* @extends {ShinyEventCommon}
*/
interface ShinyEventValue extends ShinyEventCommon {
value: unknown;
/**
* The output binding for the output that updated.
* @type {OutputBindingAdapter}
*/
binding: OutputBindingAdapter;
}
/**
* Create a shiny custom `shiny:value` event as an instance of EventValue. This
* event is triggered when an output's value changes.
*
* @class EventValue
* @typedef {EventValue}
* @extends {EventCommon}
*/
class EventValue extends EventCommon {
/**
* The `ShinyEventValue` event object.
* @type {ShinyEventValue}
*/
declare event: ShinyEventValue;
/**
* Creates an instance of EventValue.
*
* @constructor
* @param {{
name: ShinyEventValue["name"];
value: ShinyEventValue["value"];
binding: ShinyEventValue["binding"];
}} {
name,
value,
binding,
}
*/
constructor({
name,
value,
binding,
}: {
name: ShinyEventValue["name"];
value: ShinyEventValue["value"];
binding: ShinyEventValue["binding"];
}) {
super("shiny:value", name, value);
this.event.binding = binding;
}
/**
* Get the output binding for the output that updated.
* @readonly
* @type {ShinyEventValue["binding"]}
*/
get binding(): ShinyEventValue["binding"] {
return this.event.binding;
}
}
/**
* A interface for the custom `shiny:error` event.
*
* @interface ShinyEventError
* @typedef {ShinyEventError}
* @extends {ShinyEventCommon}
*/
interface ShinyEventError extends ShinyEventCommon {
/**
* The output binding for the output where the error occurred.
* @type {OutputBindingAdapter}
*/
binding: OutputBindingAdapter;
/**
* The error message.
* @type {ErrorsMessageValue}
*/
error: ErrorsMessageValue;
}
/**
* Create a shiny custom `shiny:error` event as an instance of EventError. This
* event is triggered when an error occurs while processing the reactive
* expression that produces the output.
*
* @class EventError
* @typedef {EventError}
* @extends {EventCommon}
*/
class EventError extends EventCommon {
/**
* The `ShinyEventError` event object.
* @type {ShinyEventError}
*/
declare event: ShinyEventError;
/**
* Creates an instance of EventError.
*
* @constructor
* @param {{
name: ShinyEventError["name"];
binding: ShinyEventError["binding"];
error: ShinyEventError["error"];
}} {
name,
binding,
error,
}
*/
constructor({
name,
binding,
error,
}: {
name: ShinyEventError["name"];
binding: ShinyEventError["binding"];
error: ShinyEventError["error"];
}) {
super("shiny:error", name, null);
this.event.binding = binding;
this.event.error = error;
}
/**
* Get the output binding for the output where the error occurred.
* @readonly
* @type {ShinyEventError["binding"]}
*/
get binding(): ShinyEventError["binding"] {
return this.event.binding;
}
/**
* Get the error message.
* @readonly
* @type {ShinyEventError["error"]}
*/
get error(): ShinyEventError["error"] {
return this.event.error;
}
}
/**
* A interface for the custom `shiny:message` event.
*
* @interface ShinyEventMessage
* @typedef {ShinyEventMessage}
* @extends {JQuery.Event}
*/
interface ShinyEventMessage extends JQuery.Event {
/**
* Arbitrary message data from the server. This data will ultimately be
* handled by the function provided to `Shiny.addCustomMessageHandler()` for
* the given message type.
*
* @type {{ [key: string]: unknown }}
*/
message: { [key: string]: unknown };
}
/**
* Create a shiny custom `shiny:message` event as an instance of EventMessage.
* This message is triggered when the server sends a custom message to the app
* via `session$sendCustomMessage()`.
*
* @class EventMessage
* @typedef {EventMessage}
* @extends {EventBase}
*/
class EventMessage extends EventBase {
/**
* The `ShinyEventMessage` event object.
* @type {ShinyEventMessage}
*/
declare event: ShinyEventMessage;
/**
* Creates an instance of EventMessage.
*
* @constructor
* @param {ShinyEventMessage["message"]} message
*/
constructor(message: ShinyEventMessage["message"]) {
super("shiny:message");
this.event.message = message;
}
/**
* Get the message data from the event.
* @readonly
* @type {ShinyEventMessage["message"]}
*/
get message(): ShinyEventMessage["message"] {
return this.event.message;
}
}
// Augment the JQuery interface ----------------------------------------------
// This allows extensions to use .on() in Typescript with Shiny's custom events.
// E.g. in {bslib}, we can use the following with complete type information:
//
// ```
// $(document).on("shiny:value", function(event: ShinyEventValue) { })
// ```
declare global {
interface JQuery {
on(
events: "shiny:inputchanged",
handler: EvtFn<ShinyEventInputChanged>
): this;
on(
events: "shiny:updateinput",
handler: EvtFn<ShinyEventUpdateInput>
): this;
on(events: "shiny:value", handler: EvtFn<ShinyEventValue>): this;
on(events: "shiny:error", handler: EvtFn<ShinyEventError>): this;
on(events: "shiny:message", handler: EvtFn<ShinyEventMessage>): this;
}
}
export {
EventCommon,
EventInputChanged,
EventUpdateInput,
EventValue,
EventError,
EventMessage,
};
export type {
ShinyEventInputChanged,
ShinyEventUpdateInput,

View File

@@ -1,8 +1,8 @@
import $ from "jquery";
import { triggerFileInputChanged } from "../events/inputChanged";
import { $escape } from "../utils";
import type { ShinyApp } from "../shiny/shinyapp";
import { getFileInputBinding } from "../shiny/initedMethods";
import { EventInputChanged } from "../events/shinyEvents";
type JobId = string;
type UploadUrl = string;
@@ -227,14 +227,14 @@ class FileUploader extends FileProcessor {
// Trigger shiny:inputchanged. Unlike a normal shiny:inputchanged event,
// it's not possible to modify the information before the values get
// sent to the server.
const evt = triggerFileInputChanged(
this.id,
fileInfo,
getFileInputBinding(),
this.el,
"shiny.fileupload",
document
);
const inputChangedEvent = new EventInputChanged({
name: this.id,
value: fileInfo,
el: this.el,
binding: getFileInputBinding(),
inputType: "shiny.fileupload",
});
inputChangedEvent.triggerOn(document);
this.makeRequest(
"uploadEnd",
@@ -245,7 +245,7 @@ class FileUploader extends FileProcessor {
this.$bar().text("Upload complete");
// Reset the file input's value to "". This allows the same file to be
// uploaded again. https://stackoverflow.com/a/22521275
$(evt.el as HTMLElement).val("");
$(inputChangedEvent.el as HTMLElement).val("");
},
(error) => {
this.onError(error);

View File

@@ -1,6 +1,5 @@
import $ from "jquery";
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
import type { ShinyEventInputChanged } from "../events/shinyEvents";
import { EventInputChanged } from "../events/shinyEvents";
import { splitInputNameType } from "./splitInputNameType";
class InputEventDecorator implements InputPolicy {
@@ -11,31 +10,34 @@ class InputEventDecorator implements InputPolicy {
}
setInput(nameType: string, value: unknown, opts: InputPolicyOpts): void {
const evt = $.Event("shiny:inputchanged") as ShinyEventInputChanged;
const input = splitInputNameType(nameType);
evt.name = input.name;
evt.inputType = input.inputType;
evt.value = value;
evt.binding = opts.binding || null;
evt.el = opts.el || null;
evt.priority = opts.priority;
const inputChangedEvent = new EventInputChanged({
name: input.name,
value,
el: opts.el || null,
binding: opts.binding || null,
inputType: input.inputType,
priority: opts.priority,
});
// The `shiny:inputchanged` JavaScript event now triggers on the related
// input element instead of `document`. Existing event listeners bound to
// `document` will still detect the event due to event bubbling. #2446
// If no `el` exists, use `document` instead. #3584
$(opts.el || window.document).trigger(evt);
inputChangedEvent.triggerOn(opts.el || window.document);
if (!evt.isDefaultPrevented()) {
let name = evt.name;
if (!inputChangedEvent.isDefaultPrevented()) {
let name = inputChangedEvent.name;
if (evt.inputType !== "") name += ":" + evt.inputType;
if (inputChangedEvent.inputType !== "")
name += ":" + inputChangedEvent.inputType;
// Most opts aren't passed along to lower levels in the input decorator
// stack.
this.target.setInput(name, evt.value, { priority: opts.priority });
this.target.setInput(name, inputChangedEvent.value, {
priority: opts.priority,
});
}
}
}

View File

@@ -15,11 +15,11 @@ import type { HtmlDep } from "./render";
import { hideReconnectDialog, showReconnectDialog } from "./reconnectDialog";
import { resetBrush } from "../imageutils/resetBrush";
import type { OutputBindingAdapter } from "../bindings/outputAdapter";
import type {
ShinyEventError,
ShinyEventMessage,
ShinyEventValue,
ShinyEventUpdateInput,
import {
EventValue,
EventUpdateInput,
EventError,
EventMessage,
} from "../events/shinyEvents";
import type { InputBinding } from "../bindings";
import { indirectEval } from "../utils/eval";
@@ -471,37 +471,31 @@ class ShinyApp {
delete this.$values[name];
const binding = this.$bindings[name];
const evt: ShinyEventError = $.Event("shiny:error");
evt.name = name;
evt.error = error;
evt.binding = binding;
$(binding ? binding.el : document).trigger(evt);
if (!evt.isDefaultPrevented() && binding && binding.onValueError) {
binding.onValueError(evt.error);
const errorEvent = new EventError({ name, error, binding });
errorEvent.triggerOn(binding?.el);
if (!errorEvent.isDefaultPrevented() && binding && binding.onValueError) {
binding.onValueError(errorEvent.error);
}
}
async receiveOutput<T>(name: string, value: T): Promise<T | undefined> {
const binding = this.$bindings[name];
const evt: ShinyEventValue = $.Event("shiny:value");
evt.name = name;
evt.value = value;
evt.binding = binding;
const valueEvent = new EventValue({ name, value, binding });
if (this.$values[name] === value) {
$(binding ? binding.el : document).trigger(evt);
valueEvent.triggerOn(binding?.el);
return undefined;
}
this.$values[name] = value;
delete this.$errors[name];
$(binding ? binding.el : document).trigger(evt);
valueEvent.triggerOn(binding?.el);
if (!evt.isDefaultPrevented() && binding) {
await binding.onValueChange(evt.value);
if (!valueEvent.isDefaultPrevented() && binding) {
await binding.onValueChange(valueEvent.value);
}
return value;
@@ -622,7 +616,7 @@ class ShinyApp {
// Shiny.addCustomMessageHandler = addCustomMessageHandler;
async dispatchMessage(data: ArrayBufferLike | string): Promise<void> {
let msgObj: ShinyEventMessage["message"] = {};
let msgObj: EventMessage["message"] = {};
if (typeof data === "string") {
msgObj = JSON.parse(data);
@@ -643,15 +637,13 @@ class ShinyApp {
msgObj.custom[type] = data;
}
const evt: ShinyEventMessage = $.Event("shiny:message");
evt.message = msgObj;
$(document).trigger(evt);
if (evt.isDefaultPrevented()) return;
const messageEvent = new EventMessage({ message: msgObj });
messageEvent.triggerOn(document);
if (messageEvent.isDefaultPrevented()) return;
// Send msgObj.foo and msgObj.bar to appropriate handlers
await this._sendMessagesToHandlers(
evt.message,
messageEvent.message,
messageHandlers,
messageHandlerOrder
);
@@ -720,13 +712,13 @@ class ShinyApp {
if ($obj.length > 0) {
if (!$obj.attr("aria-live")) $obj.attr("aria-live", "polite");
const el = $obj[0];
const evt: ShinyEventUpdateInput = $.Event("shiny:updateinput");
evt.message = message[i].message;
evt.binding = inputBinding;
$(el).trigger(evt);
if (!evt.isDefaultPrevented())
inputBinding.receiveMessage(el, evt.message);
const updateInputEvent = new EventUpdateInput({
message: message[i].message,
binding: inputBinding,
});
updateInputEvent.triggerOn(el);
if (!updateInputEvent.isDefaultPrevented())
inputBinding.receiveMessage(el, updateInputEvent.message);
}
}
}
@@ -1225,10 +1217,10 @@ class ShinyApp {
// value for the tabset gets updated (i.e. input$tabsetId
// should be null if there are no tabs).
const destTabValue = getFirstTab($tabset);
const evt: ShinyEventUpdateInput = $.Event("shiny:updateinput");
evt.binding = inputBinding;
$tabset.trigger(evt);
const updateInputEvent = new EventUpdateInput({
binding: inputBinding,
});
updateInputEvent.triggerOn($tabset);
inputBinding.setValue($tabset[0], destTabValue);
}
}

View File

@@ -17,4 +17,4 @@ declare global {
on(events: `shown.bs.${string}.sendImageSize`, selector: string, handler: (this: HTMLElement, e: JQueryEventHandlerBase<HTMLElement, any>) => void): this;
}
}
export {};
export type { EvtFn };

View File

@@ -3,32 +3,447 @@ import type { InputBinding } from "../bindings/input/inputBinding";
import type { OutputBindingAdapter } from "../bindings/outputAdapter";
import type { EventPriority } from "../inputPolicies/inputPolicy";
import type { ErrorsMessageValue } from "../shiny/shinyapp";
import type { EvtFn } from "./jQueryEvents";
/**
* Shiny Event Base
*
* This class implements a common interface for all Shiny events, and provides a
* layer of abstraction between the Shiny event and the underlying jQuery event
* object. We use a new class, rather than extending JQuery.Event, because
* JQuery.Event is an old function-style class. Each Event class has a
* corresponding ShinyEvent interface that describes the event object that is
* emitted. At the end of this file, we extend JQuery's `on()` method to
* associate the ShinyEvent interfaces with their corresponding event string.
*
* @class EventBase
* @typedef {EventBase}
*/
declare class EventBase {
/**
* The underlying jQuery event object wrapped by `EventBase`.
* @type {JQuery.Event}
*/
event: JQuery.Event;
/**
* Creates an instance of EventBase.
*
* @constructor
* @param {string} type - The event type.
*/
constructor(type: string);
/**
* Trigger the event on an element or the document.
*
* @example
* // Instead of this...
* el.trigger(shinyEvent);
* // ...do this
* shinyEvent.triggerOn(el);
*
* @param {(HTMLElement | JQuery<HTMLElement> | typeof document | null)} el -
* The element to trigger the event on, or `null` to trigger on `document`.
*/
triggerOn(el: HTMLElement | JQuery<HTMLElement> | typeof document | null): void;
/**
* Proxy for `event.preventDefault()`.
*
* @returns {boolean} `true` if the default action was prevented, `false`
* otherwise.
*/
isDefaultPrevented(): boolean;
}
/**
* A common interface for most Shiny events.
*
* @interface ShinyEventCommon
* @typedef {ShinyEventCommon}
* @extends {JQuery.Event}
*/
interface ShinyEventCommon extends JQuery.Event {
/**
* The event name.
* @type {string}
*/
name: string;
value: unknown;
el: HTMLElement | null;
/**
* Event value containing arbitrary event data.
* @type {*}
*/
value: any;
}
/**
* Create a common Shiny event.
*
* @class EventCommon
* @typedef {EventCommon}
* @extends {EventBase}
*/
declare class EventCommon extends EventBase {
/**
* The actual event object.
* @type {ShinyEventCommon}
*/
event: ShinyEventCommon;
/**
* Creates an instance of EventCommon.
*
* @constructor
* @param {ShinyEventCommon["type"]} type - The Shiny custom event type, e.g.
* `shiny:value`.
* @param {ShinyEventCommon["name"]} name - The event name.
* @param {ShinyEventCommon["value"]} value - The event value, or arbitrary
* data included with the event.
*/
constructor(type: ShinyEventCommon["type"], name: ShinyEventCommon["name"], value: ShinyEventCommon["value"]);
/**
* Get the event name.
* @readonly
* @type {ShinyEventCommon["name"]}
*/
get name(): ShinyEventCommon["name"];
/**
* Get the event value.
* @readonly
* @type {ShinyEventCommon["value"]}
*/
get value(): ShinyEventCommon["value"];
}
/**
* An interface for the `shiny:inputchanged` event.
*
* @interface ShinyEventInputChanged
* @typedef {ShinyEventInputChanged}
* @extends {ShinyEventCommon}
*/
interface ShinyEventInputChanged extends ShinyEventCommon {
value: unknown;
/**
* The input element whose value has changed.
* @type {(HTMLElement | null)}
*/
el: HTMLElement | null;
/**
* The input binding for the changed input.
* @type {(InputBinding | null)}
*/
binding: InputBinding | null;
/**
* The input type.
* @type {string}
*/
inputType: string;
priority: EventPriority;
/**
* The input event priority.
* @type {?EventPriority}
*/
priority?: EventPriority;
}
/**
* Create a custom `shiny:inputchanged` event as an instance of
* EventInputChanged.
*
* @class EventInputChanged
* @typedef {EventInputChanged}
* @extends {EventCommon}
*/
declare class EventInputChanged extends EventCommon {
/**
* The `ShinyEventInputChanged` event object.
* @type {ShinyEventInputChanged}
*/
event: ShinyEventInputChanged;
/**
* Creates an instance of EventInputChanged.
*
* @constructor
* @param {{
name: ShinyEventInputChanged["name"];
value: ShinyEventInputChanged["value"];
el: ShinyEventInputChanged["el"];
binding: ShinyEventInputChanged["binding"];
inputType: ShinyEventInputChanged["inputType"];
priority?: ShinyEventInputChanged["priority"];
}} {
name,
value,
el,
binding,
inputType,
priority,
}
*/
constructor({ name, value, el, binding, inputType, priority, }: {
name: ShinyEventInputChanged["name"];
value: ShinyEventInputChanged["value"];
el: ShinyEventInputChanged["el"];
binding: ShinyEventInputChanged["binding"];
inputType: ShinyEventInputChanged["inputType"];
priority?: ShinyEventInputChanged["priority"];
});
/**
* Get the input element whose value has changed.
* @readonly
* @type {ShinyEventInputChanged["el"]}
*/
get el(): ShinyEventInputChanged["el"];
/**
* Get the input binding for the changed input.
* @readonly
* @type {ShinyEventInputChanged["binding"]}
*/
get binding(): ShinyEventInputChanged["binding"];
/**
* Get the input type.
* @readonly
* @type {ShinyEventInputChanged["inputType"]}
*/
get inputType(): ShinyEventInputChanged["inputType"];
/**
* Get the input event priority.
* @readonly
* @type {ShinyEventInputChanged["priority"]}
*/
get priority(): ShinyEventInputChanged["priority"];
}
/**
* A interface for the custom `shiny:updateinput` event.
*
* @interface ShinyEventUpdateInput
* @typedef {ShinyEventUpdateInput}
* @extends {ShinyEventCommon}
*/
interface ShinyEventUpdateInput extends ShinyEventCommon {
message: unknown;
/**
* Arbitrary message data, typically sent from the server, to be processed by
* the `receiveMessage` method of the input binding.
* @type {?*}
*/
message?: any;
/**
* The input binding for the input.
* @type {InputBinding}
*/
binding: InputBinding;
}
/**
* Create a shiny custom `shiny:updateinput` event as an instance of
* EventUpdateInput. This event carries message data from the server, sent via
* `session$sendInputMessage()`, to the input binding's `receiveMessage` method.
*
* @class EventUpdateInput
* @typedef {EventUpdateInput}
* @extends {EventBase}
*/
declare class EventUpdateInput extends EventBase {
/**
* The `ShinyEventUpdateInput` event object.
* @type {ShinyEventUpdateInput}
*/
event: ShinyEventUpdateInput;
/**
* Creates an instance of EventUpdateInput.
*
* @constructor
* @param {{
message?: ShinyEventUpdateInput["message"];
binding: ShinyEventUpdateInput["binding"];
}} {
message,
binding,
}
*/
constructor({ message, binding, }: {
message?: ShinyEventUpdateInput["message"];
binding: ShinyEventUpdateInput["binding"];
});
/**
* Get the `shiny:updateinput` message data.
* @readonly
* @type {ShinyEventUpdateInput["message"]}
*/
get message(): ShinyEventUpdateInput["message"];
/**
* Get the input binding for the input.
* @readonly
* @type {ShinyEventUpdateInput["binding"]}
*/
get binding(): ShinyEventUpdateInput["binding"];
}
/**
* A interface for the custom `shiny:value` event.
*
* @interface ShinyEventValue
* @typedef {ShinyEventValue}
* @extends {ShinyEventCommon}
*/
interface ShinyEventValue extends ShinyEventCommon {
value: unknown;
/**
* The output binding for the output that updated.
* @type {OutputBindingAdapter}
*/
binding: OutputBindingAdapter;
}
/**
* Create a shiny custom `shiny:value` event as an instance of EventValue. This
* event is triggered when an output's value changes.
*
* @class EventValue
* @typedef {EventValue}
* @extends {EventCommon}
*/
declare class EventValue extends EventCommon {
/**
* The `ShinyEventValue` event object.
* @type {ShinyEventValue}
*/
event: ShinyEventValue;
/**
* Creates an instance of EventValue.
*
* @constructor
* @param {{
name: ShinyEventValue["name"];
value: ShinyEventValue["value"];
binding: ShinyEventValue["binding"];
}} {
name,
value,
binding,
}
*/
constructor({ name, value, binding, }: {
name: ShinyEventValue["name"];
value: ShinyEventValue["value"];
binding: ShinyEventValue["binding"];
});
/**
* Get the output binding for the output that updated.
* @readonly
* @type {ShinyEventValue["binding"]}
*/
get binding(): ShinyEventValue["binding"];
}
/**
* A interface for the custom `shiny:error` event.
*
* @interface ShinyEventError
* @typedef {ShinyEventError}
* @extends {ShinyEventCommon}
*/
interface ShinyEventError extends ShinyEventCommon {
/**
* The output binding for the output where the error occurred.
* @type {OutputBindingAdapter}
*/
binding: OutputBindingAdapter;
/**
* The error message.
* @type {ErrorsMessageValue}
*/
error: ErrorsMessageValue;
}
/**
* Create a shiny custom `shiny:error` event as an instance of EventError. This
* event is triggered when an error occurs while processing the reactive
* expression that produces the output.
*
* @class EventError
* @typedef {EventError}
* @extends {EventCommon}
*/
declare class EventError extends EventCommon {
/**
* The `ShinyEventError` event object.
* @type {ShinyEventError}
*/
event: ShinyEventError;
/**
* Creates an instance of EventError.
*
* @constructor
* @param {{
name: ShinyEventError["name"];
binding: ShinyEventError["binding"];
error: ShinyEventError["error"];
}} {
name,
binding,
error,
}
*/
constructor({ name, binding, error, }: {
name: ShinyEventError["name"];
binding: ShinyEventError["binding"];
error: ShinyEventError["error"];
});
/**
* Get the output binding for the output where the error occurred.
* @readonly
* @type {ShinyEventError["binding"]}
*/
get binding(): ShinyEventError["binding"];
/**
* Get the error message.
* @readonly
* @type {ShinyEventError["error"]}
*/
get error(): ShinyEventError["error"];
}
/**
* A interface for the custom `shiny:message` event.
*
* @interface ShinyEventMessage
* @typedef {ShinyEventMessage}
* @extends {JQuery.Event}
*/
interface ShinyEventMessage extends JQuery.Event {
/**
* Arbitrary message data from the server. This data will ultimately be
* handled by the function provided to `Shiny.addCustomMessageHandler()` for
* the given message type.
*
* @type {{ [key: string]: unknown }}
*/
message: {
[key: string]: unknown;
};
}
/**
* Create a shiny custom `shiny:message` event as an instance of EventMessage.
* This message is triggered when the server sends a custom message to the app
* via `session$sendCustomMessage()`.
*
* @class EventMessage
* @typedef {EventMessage}
* @extends {EventBase}
*/
declare class EventMessage extends EventBase {
/**
* The `ShinyEventMessage` event object.
* @type {ShinyEventMessage}
*/
event: ShinyEventMessage;
/**
* Creates an instance of EventMessage.
*
* @constructor
* @param {ShinyEventMessage["message"]} message
*/
constructor(message: ShinyEventMessage["message"]);
/**
* Get the message data from the event.
* @readonly
* @type {ShinyEventMessage["message"]}
*/
get message(): ShinyEventMessage["message"];
}
declare global {
interface JQuery {
on(events: "shiny:inputchanged", handler: EvtFn<ShinyEventInputChanged>): this;
on(events: "shiny:updateinput", handler: EvtFn<ShinyEventUpdateInput>): this;
on(events: "shiny:value", handler: EvtFn<ShinyEventValue>): this;
on(events: "shiny:error", handler: EvtFn<ShinyEventError>): this;
on(events: "shiny:message", handler: EvtFn<ShinyEventMessage>): this;
}
}
export { EventCommon, EventInputChanged, EventUpdateInput, EventValue, EventError, EventMessage, };
export type { ShinyEventInputChanged, ShinyEventUpdateInput, ShinyEventValue, ShinyEventError, ShinyEventMessage, };

View File

@@ -1137,6 +1137,20 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-typescript@npm:^7.21.3":
version: 7.21.3
resolution: "@babel/plugin-transform-typescript@npm:7.21.3"
dependencies:
"@babel/helper-annotate-as-pure": ^7.18.6
"@babel/helper-create-class-features-plugin": ^7.21.0
"@babel/helper-plugin-utils": ^7.20.2
"@babel/plugin-syntax-typescript": ^7.20.0
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: c16fd577bf43f633deb76fca2a8527d8ae25968c8efdf327c1955472c3e0257e62992473d1ad7f9ee95379ce2404699af405ea03346055adadd3478ad0ecd117
languageName: node
linkType: hard
"@babel/plugin-transform-unicode-escapes@npm:^7.18.10":
version: 7.18.10
resolution: "@babel/plugin-transform-unicode-escapes@npm:7.18.10"
@@ -2180,6 +2194,7 @@ __metadata:
dependencies:
"@babel/core": ^7.14.3
"@babel/plugin-proposal-class-properties": ^7.13.0
"@babel/plugin-transform-typescript": ^7.21.3
"@babel/preset-env": ^7.14.2
"@babel/preset-typescript": ^7.13.0
"@babel/runtime": ^7.14.0