fvtt-types
Version:
TypeScript type definitions for Foundry VTT
646 lines (569 loc) • 24.7 kB
text/typescript
import type {
AnyArray,
AnyConstructor,
AnyFunction,
AnyObject,
Coalesce,
DeepPartial,
EmptyObject,
GetKey,
Identity,
InexactPartial,
IntentionalPartial,
MaybePromise,
NullishCoalesce,
SimpleMerge,
} from "#utils";
import type ApplicationV2 from "./application.d.mts";
declare module "#configuration" {
namespace Hooks {
interface ApplicationV2Config {
DialogV2: DialogV2.Any;
}
}
}
type DeepInexactPartial<T> = T extends object
? T extends AnyArray | AnyFunction | AnyConstructor
? T
: {
[K in keyof T]?: DeepInexactPartial<T[K]> | undefined;
}
: T;
/**
* A lightweight Application that renders a dialog containing a form with arbitrary content, and some buttons.
*
* @example Prompt the user to confirm an action.
* ```js
* const proceed = await foundry.applications.api.DialogV2.confirm({
* content: "Are you sure?",
* rejectClose: false,
* modal: true
* });
* if ( proceed ) console.log("Proceed.");
* else console.log("Do not proceed.");
* ```
*
* @example Prompt the user for some input.
* ```js
* let guess;
* try {
* guess = await foundry.applications.api.DialogV2.prompt({
* window: { title: "Guess a number between 1 and 10" },
* content: '<input name="guess" type="number" min="1" max="10" step="1" autofocus>',
* ok: {
* label: "Submit Guess",
* callback: (event, button, dialog) => button.form.elements.guess.valueAsNumber
* }
* });
* } catch {
* console.log("User did not make a guess.");
* return;
* }
* const n = Math.ceil(CONFIG.Dice.randomUniform() * 10);
* if ( n === guess ) console.log("User guessed correctly.");
* else console.log("User guessed incorrectly.");
* ```
*
* @example A custom dialog.
* ```js
* new foundry.applications.api.DialogV2({
* window: { title: "Choose an option" },
* content: `
* <label><input type="radio" name="choice" value="one" checked> Option 1</label>
* <label><input type="radio" name="choice" value="two"> Option 2</label>
* <label><input type="radio" name="choice" value="three"> Options 3</label>
* `,
* buttons: [{
* action: "choice",
* label: "Make Choice",
* default: true,
* callback: (event, button, dialog) => button.form.elements.choice.value
* }, {
* action: "all",
* label: "Take All"
* }],
* submit: result => {
* if ( result === "all" ) console.log("User picked all options.");
* else console.log(`User picked option: ${result}`);
* }
* }).render({ force: true });
* ```
*/
declare class DialogV2<
RenderContext extends object = EmptyObject,
// TODO(LukeAbby): The `any` is unideal but it's to to stymy a circularity when it's `DialogV2`.
Configuration extends DialogV2.Configuration = DialogV2.Configuration<any>,
RenderOptions extends DialogV2.RenderOptions = DialogV2.RenderOptions,
> extends ApplicationV2<RenderContext, Configuration, RenderOptions> {
static override DEFAULT_OPTIONS: DialogV2.DefaultOptions;
protected override _initializeApplicationOptions(options: DeepPartial<Configuration>): Configuration;
/**
* @remarks Note: fvtt-types assumes that the default form contains no values. Specifically this
* is important for `DialogV2.input` where it assumes that given no `content` it can return `{}`.
* In theory you could override `_renderHTML` to add a custom `form`.
*
* This could make `DialogV2.input` incorrectly typed but it is assumed that such a use case would
* be better served with a custom `ApplicationV2` subclass. Therefore this use case isn't
* accounted for. However if it would be important to you, please let us know.
*/
protected override _renderHTML(
_context: RenderContext,
_options: DeepPartial<RenderOptions>,
): Promise<HTMLFormElement>;
/**
* Render configured buttons.
*/
protected _renderButtons(): string;
/**
* Handle submitting the dialog.
* @param target - The button that was clicked or the default button.
* @param event - The triggering event.
*/
protected _onSubmit(target: HTMLButtonElement, event: PointerEvent | SubmitEvent): Promise<this>;
protected override _onFirstRender(
_context: DeepPartial<RenderContext>,
_options: DeepPartial<RenderOptions>,
): Promise<void>;
protected override _replaceHTML(
result: HTMLFormElement,
content: HTMLElement,
_options: DeepPartial<RenderOptions>,
): void;
/**
* Handle keypresses within the dialog.
* @param event - The triggering event.
*/
protected _onKeyDown(event: KeyboardEvent): void;
/**
* @param event - The originating click event.
* @param target - The button element that was clicked.
*/
protected static _onClickButton(this: DialogV2.Any, event: PointerEvent, target: HTMLButtonElement): void;
/**
* A utility helper to generate a dialog with yes and no buttons.
* @returns Resolves to true if the yes button was pressed, or false if the no button
* was pressed. If additional buttons were provided, the Promise resolves to
* the identifier of the one that was pressed, or the value returned by its
* callback. If the dialog was dismissed, and rejectClose is false, the
* Promise resolves to null.
*
* @remarks The callbacks within `config.buttons` are called with an instance of the current class.
* While many users likely will not notice, if this ends up effecting you then you will need to
* make an override to provide the current class. For example:
* ```typescript
* class YourDialog extends DialogV2 {
* static confirm<const Config extends DialogV2.ConfirmConfig<YourDialog> | undefined = undefined>(
* config?: Config,
* ): Promise<DialogV2.ConfirmReturn<Config>> {
* return super.confirm(config);
* };
* }
* ```
*/
static confirm<const Config extends DialogV2.ConfirmConfig | undefined = undefined>(
config?: Config,
): Promise<DialogV2.ConfirmReturn<Config>>;
/**
* A utility helper to generate a dialog with a single confirmation button.
* @returns - Resolves to the identifier of the button used to submit the dialog,
* or the value returned by that button's callback. If the dialog was
* dismissed, and rejectClose is false, the Promise resolves to null.
*
* @remarks The callbacks within `config.buttons` are called with an instance of the current class.
* While many users likely will not notice, if this ends up effecting you then you will need to
* make an override to provide the current class. For example:
* ```typescript
* class YourDialog extends DialogV2 {
* static prompt<const Config extends DialogV2.PromptConfig | undefined = undefined>(
* config?: Config,
* ): Promise<DialogV2.PromptReturn<Config>> {
* return super.prompt(config);
* };
* }
* ```
*/
static prompt<const Config extends DialogV2.PromptConfig | undefined = undefined>(
config?: Config,
): Promise<DialogV2.PromptReturn<Config>>;
/**
* A utility helper to generate a dialog for user input.
* @param config - Options to overwrite the default confirmation button configuration.
* @returns Resolves to the data of the form if the ok button was pressed,
* or the value returned by that button's callback. If additional
* buttons were provided, the Promise resolves to the identifier of
* the one that was pressed, or the value returned by its callback.
* If the dialog was dismissed, and rejectClose is false, the Promise
* resolves to null.
*
* @remarks `input` by default returns form data derived from `config.content`. Unfortunately this
* means that short of writing an HTML parser to support automatically deriving this, the caller
* must hint at the return type. Specifically a call should look something like this:
* ```typescript
* DialogV2.input({
* content: `<form>
* <label><input type="radio" name="choice" value="one" checked> Option 1</label>
* <label><input type="radio" name="choice" value="two"> Option 2</label>
* <label><input type="radio" name="choice" value="three"> Options 3</label>
* </form>` as DialogV2.Content<{ choice: "one" | "two" | "three" }>;
* });
* ```
* The added `as DialogV2.Content` allows fvtt-types to extract out the shape of the form content.
* This gives you the correct return type instead of `AnyObject`.
*
* Additionally, the callbacks within `config.buttons` are called with an instance of the current class.
* While many users likely will not notice, if this ends up effecting you then you will need to
* make an override to provide the current class. For example:
* ```typescript
* class YourDialog extends DialogV2 {
* static input<const Config extends DialogV2.InputConfig<YourDialog> | undefined = undefined>(
* config?: Config,
* ): Promise<DialogV2.InputReturn<Config>> {
* return super.input(config);
* };
* }
* ```
*/
static input<const Config extends DialogV2.InputConfig | undefined = undefined>(
config?: Config,
): Promise<DialogV2.InputReturn<Config>>;
/**
* Spawn a dialog and wait for it to be dismissed or submitted.
* @returns Resolves to the identifier of the button used to submit the
* dialog, or the value returned by that button's callback. If the
* dialog was dismissed, and rejectClose is false, the Promise
* resolves to null.
*
* @remarks The callbacks within `config.buttons` are called with an instance of the current class.
* While many users likely will not notice, if this ends up effecting you then you will need to
* make an override to provide the current class. For example:
* ```typescript
* class YourDialog extends DialogV2 {
* static wait<const Config extends DialogV2.WaitOptions<YourDialog>>(
* config: Config,
* ): Promise<DialogV2.WaitReturn<Config>> {
* return super.wait(config);
* };
* }
* ```
*/
static wait<const Config extends DialogV2.WaitOptions>(config: Config): Promise<DialogV2.WaitReturn<Config>>;
/**
* Present an asynchronous Dialog query to a specific User for response.
* @param user - A User instance or a User id
* @param type - The type of Dialog to present
* @param config - Dialog configuration forwarded on to the Dialog.prompt, Dialog.confirm, or
* Dialog.wait function depending on the query type. Callback options are not supported.
* @returns The query response or null if no response was provided
*
* @see {@link DialogV2.prompt}
* @see {@link DialogV2.confirm}
* @see {@link DialogV2.wait}
*
* @remarks The callbacks within `config.buttons` are called with an instance of the current class.
* While many users likely will not notice, if this ends up effecting you then you will need to
* make an override to provide the current class. For example:
* ```typescript
* class YourDialog extends DialogV2 {
* static query<T extends DialogV2.Type, const Options extends DialogV2.QueryConfig<T>>(
* user: User.Implementation | string,
* type: T,
* config: Options,
* ): Promise<DialogV2.QueryReturn<T, Options>> {
* return super.query(user, type, config);
* };
* }
* ```
*/
static query<T extends DialogV2.Type, const Options extends DialogV2.QueryConfig<T>>(
user: User.Implementation | string,
type: T,
config: Options,
): Promise<DialogV2.QueryReturn<T, Options>>;
/**
* The dialog query handler.
*/
static _handleQuery<T extends DialogV2.Type, const Options extends DialogV2.QueryConfig<T>>(
config: DialogV2.HandleQueryConfig<T, Options>,
): Promise<DialogV2.QueryReturn<T, Options>>;
}
declare namespace DialogV2 {
interface Any extends AnyDialogV2 {}
interface AnyConstructor extends Identity<typeof AnyDialogV2> {}
interface Button<Dialog extends DialogV2.Any = DialogV2.Any> {
/**
* The button action identifier.
*/
action: string;
/**
* The button label. Will be localized.
*/
label: string;
/**
* FontAwesome icon classes.
*/
icon?: string | undefined;
/**
* CSS classes to apply to the button.
*/
class?: string | undefined;
/**
* CSS style to apply to the button.
* @defaultValue `{}`
*/
style?: Record<string, string> | undefined;
/**
* The button type.
* @defaultValue `"submit"`
*/
type?: HTMLButtonElement["type"] | undefined;
/**
* Whether this button represents the default action to take if the user
* submits the form without pressing a button, i.e. with an Enter
* keypress.
*/
default?: boolean | undefined;
/**
* A function to invoke when the button is clicked. The value returned
* from this function will be used as the dialog's submitted value.
* Otherwise, the button's identifier is used.
*/
callback?: ButtonCallback<Dialog> | undefined;
}
type ButtonCallback<Dialog extends DialogV2.Any = DialogV2.Any> = (
event: PointerEvent | SubmitEvent,
button: HTMLButtonElement,
dialog: Dialog,
) => MaybePromise<unknown>;
interface RenderContext extends ApplicationV2.RenderContext {}
interface Configuration<Dialog extends DialogV2.Any = DialogV2.Any> extends ApplicationV2.Configuration<Dialog> {
/**
* Modal dialogs prevent interaction with the rest of the UI until they are dismissed or submitted.
*/
modal?: boolean | null | undefined;
/**
* Button configuration.
* @remarks Must have at least one button or else `DialogV2#_initializeApplicationOptions` will
* throw.
*/
buttons: Button<Dialog>[];
/**
* The dialog content: a HTML string or a <div> element.
* If string, the content is cleaned with {@link foundry.utils.cleanHTML}.
* Otherwise, the content is not cleaned.
* @defaultValue `''`
*/
content: string | HTMLDivElement | Content<AnyObject>;
/**
* A function to invoke when the dialog is submitted.
* This will not be called if the dialog is dismissed.
*/
// TODO(LukeAbby): This will probably never be sufficiently typed.
submit?: SubmitCallback<unknown, Dialog> | null | undefined;
}
// Note(LukeAbby): This `& object` is so that the `DEFAULT_OPTIONS` can be overridden more easily
// Without it then `static override DEFAULT_OPTIONS = { unrelatedProp: 123 }` would error.
type DefaultOptions<Dialog extends DialogV2.Any = DialogV2.Any> = DeepPartial<Configuration<Dialog>> & object;
interface RenderOptions extends ApplicationV2.RenderOptions {}
type Content<Data extends AnyObject, BaseType extends string | HTMLDivElement = string | HTMLDivElement> = BaseType &
Internal.FormContent<Data>;
type RenderCallback = (event: Event, dialog: DialogV2) => void;
type CloseCallback = (event: Event, dialog: DialogV2.Any) => unknown;
type SubmitCallback<Result, Dialog extends DialogV2.Any = DialogV2.Any> = (
result: Result,
dialog: Dialog,
) => Promise<void>;
interface WaitOptions<Dialog extends DialogV2.Any = DialogV2.Any> extends DeepInexactPartial<Configuration<Dialog>> {
/**
* A synchronous function to invoke whenever the dialog is rendered.
*/
render?: RenderCallback | null | undefined;
/**
* A synchronous function to invoke when the dialog is closed under any circumstances.
*/
close?: CloseCallback | null | undefined;
/**
* Throw a Promise rejection if the dialog is dismissed.
* @defaultValue `false`
* @remarks `null` equivalent to `false`
*/
rejectClose?: boolean | null | undefined;
// TODO(LukeAbby): Once ApplicationV2's required options infrastructure is set up this shouldn't
// be necessary.
buttons: Button<Dialog>[];
}
/** @internal */
interface _PartialButtons<Dialog extends DialogV2.Any = DialogV2.Any>
extends Omit<WaitOptions<Dialog>, "buttons">,
InexactPartial<Pick<WaitOptions<Dialog>, "buttons">> {}
// Note(LukeAbby): `IntentionalPartial` is used for all the buttons because `mergeObject` is
// called. For example `{ action: undefined }` would be a logical bug.
interface ConfirmConfig<Dialog extends DialogV2.Any = DialogV2.Any> extends _PartialButtons<Dialog> {
/** Options to overwrite the default yes button configuration. */
yes?: IntentionalPartial<Button<Dialog>> | null | undefined;
/** Options to overwrite the default no button configuration. */
no?: IntentionalPartial<Button<Dialog>> | null | undefined;
}
interface PromptConfig<Dialog extends DialogV2.Any = DialogV2.Any> extends _PartialButtons<Dialog> {
/** Options to overwrite the default confirmation button configuration. */
ok?: IntentionalPartial<Button<Dialog>> | null | undefined;
}
type FormContent<FormData extends object> = (string | HTMLDivElement) & { " __fvtt_types_form_data": FormData };
/** @typeParam FD - The form data */
interface InputConfig<Dialog extends DialogV2.Any = DialogV2.Any> extends PromptConfig<Dialog> {}
type Type = "prompt" | "confirm" | "wait" | "input";
/**
* @remarks Query gets passed through a socket which means it can't take a callback function on its buttons
*/
type QueryConfig<T extends Type, Dialog extends DialogV2.Any = DialogV2.Any> =
| (T extends "wait" ? Internal.QueryWaitOptions<Dialog> : never)
| (T extends "prompt" ? Internal.QueryPromptConfig<Dialog> : never)
| (T extends "confirm" ? Internal.QueryConfirmConfig<Dialog> : never)
| (T extends "input" ? Internal.QueryInputConfig<Dialog> : never);
// Note(LukeAbby): The usage of `<never>` is because `WaitOptions` is contravariant over `Dialog`.
// This applies in many different places but is only noted here.
type WaitReturn<Options extends WaitOptions<never>> = Internal.WaitReturn<Options>;
interface ConfirmYesButton {
action: "yes";
label: "Yes";
icon: "fa-solid fa-check";
callback: () => true;
}
interface ConfirmNoButton {
action: "no";
label: "No";
icon: "fa-solid fa-xmark";
default: true;
callback: () => false;
}
type ConfirmReturn<Options extends ConfirmConfig<never> | undefined> =
Options extends ConfirmConfig<never>
? WaitReturn<
{
buttons: [
Internal.MergePartial<ConfirmYesButton, Options["yes"]>,
Internal.MergePartial<ConfirmNoButton, Options["no"]>,
...Coalesce<Options["buttons"], []>,
];
} & Omit<Options, "buttons">
>
: WaitReturn<{ buttons: [ConfirmYesButton, ConfirmNoButton] }>;
interface PromptOkButton {
action: "ok";
label: "Confirm";
icon: "fa-solid fa-check";
default: true;
}
type PromptReturn<Config extends PromptConfig<never> | undefined> =
Config extends PromptConfig<never>
? Internal.WaitReturn<
SimpleMerge<
Config,
{
buttons: [
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
Internal.MergePartial<PromptOkButton, GetKey<Config, "ok", {}>>,
...Coalesce<GetKey<Config, "buttons", undefined>, []>,
];
}
>
>
: WaitReturn<{ buttons: [PromptOkButton] }>;
type InputReturn<Config extends PromptConfig<never> | undefined> =
Config extends PromptConfig<never>
? PromptReturn<
{
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
ok: SimpleMerge<{ callback: () => Internal.ContentFormData<Config> }, GetKey<Config, "ok", {}>>;
} & Omit<Config, "ok">
>
: PromptReturn<{ ok: { callback: () => EmptyObject } }>;
interface HandleQueryConfig<T extends DialogV2.Type, Options extends DialogV2.QueryConfig<T>> {
type: T;
config: Options;
}
type QueryReturn<T extends Type, Options extends QueryConfig<T, never>> =
| (T extends "prompt" ? PromptReturn<Options> : never)
| (T extends "confirm" ? ConfirmReturn<Options> : never)
// Note(LukeAbby): `Internal.WaitReturn` is used to get around the fact that TypeScript can't be
// sure that `buttons` is required in `QueryConfig<T, never>` if `T = "wait"`. TypeScript is
// technically correct as this does subvert the soundness as if `T = "wait" | OtherType` then
// invalid options would be possible.
| (T extends "wait" ? Internal.WaitReturn<Options> : never)
| (T extends "input" ? InputReturn<Options> : never);
namespace Internal {
const __Configuration: typeof ApplicationV2.Internal.__Configuration;
const __RenderContext: typeof ApplicationV2.Internal.__RenderContext;
const __RenderOptions: typeof ApplicationV2.Internal.__RenderOptions;
type WaitReturn<Options> = Internal.ButtonReturnType<Options> | Internal.DismissType<Options>;
type DismissType<Options> = Options extends {
readonly rejectClose: true;
}
? never
: "close" extends keyof Options
? Options["close"] extends (...args: never) => infer Return
? NullishCoalesce<Return, null>
: null
: null;
type ButtonReturnType<Options> =
GetKey<Options, "buttons", undefined> extends ReadonlyArray<infer B extends Button<never>>
? B extends unknown
? OneButtonReturnType<B["callback"], B["action"]>
: never
: undefined;
type OneButtonReturnType<Callback, Action> = Callback extends () => infer Return ? Return : Action;
type ConfirmReturnType<Options extends ConfirmConfig<never> | undefined> =
| (Options extends { readonly yes: { readonly callback: ButtonCallback<infer YesReturn> } }
? NullishCoalesce<YesReturn, true>
: true)
| (Options extends { readonly no: { readonly callback: ButtonCallback<infer NoReturn> } }
? NullishCoalesce<NoReturn, false>
: false);
type ContentFormData<Config extends PromptConfig<never> | undefined> = GetFormContent<
GetKey<Config, "content", undefined>
>;
// Note(LukeAbby): The constraint should be `string | HTMLDivElement | Content<AnyObject> | undefined`
// but currently it's actually `DeepInexactPartial<HTMLDivElement>` which, needless to say, is incorrect.
// However this will require an `ApplicationV2` refactor to fix.
type GetFormContent<C> = C extends Content<infer Content> ? Content : C extends undefined ? EmptyObject : AnyObject;
interface NoCallbackButton extends Button<never> {
/**
* @deprecated A callback is not allowed in `query` as the data must all be serializable.
*/
callback?: never;
}
interface QueryWaitOptions<Dialog extends DialogV2.Any> extends WaitOptions<Dialog> {
buttons: NoCallbackButton[];
}
interface QueryPromptConfig<Dialog extends DialogV2.Any> extends PromptConfig<Dialog> {
buttons?: NoCallbackButton[];
}
interface QueryConfirmConfig<Dialog extends DialogV2.Any> extends ConfirmConfig<Dialog> {
buttons?: NoCallbackButton[];
}
interface QueryInputConfig<Dialog extends DialogV2.Any> extends InputConfig<Dialog> {
buttons?: NoCallbackButton[];
}
// Merges `T` and `U` while assuming `U` is essentially `Partial<T>`.
// The key detail is that `MergePartial<{ prop: number }, { prop?: string }>` results in `{ prop: string | number }`
//
// This is useful in `ConfirmReturn` as it allows modelling the _valid_ uses of `mergeObject` in
// `DialogV2.confirm`.
// May need to be polished later.
type MergePartial<T, U> = {
[K in keyof T]: U extends { readonly [_ in K]: infer V }
? // The additional `& {}` is useless with `exactOptionalPropertyTypes` set to `true`, the
// default, but unfortunately `{ prop: undefined }` is valid for `{ prop?: T }` with
// `exactOptionalPropertyTypes` set to false. Otherwise would cause a required prop like
// `{ action: string }` to be able to be `{ action: string | undefined }`.
V & {}
: T[K] | (U extends { readonly [_ in K]?: infer V } ? V & {} : never);
} & Omit<U, keyof T>;
class FormContent<Content extends AnyObject> {
#content: Content;
}
}
}
declare abstract class AnyDialogV2 extends DialogV2<object, DialogV2.Configuration, DialogV2.RenderOptions> {
constructor(...args: never);
}
export default DialogV2;