mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-13 01:53:30 +01:00
refactor: builders (#10448)
BREAKING CHANGE: formatters export removed (prev. deprecated) BREAKING CHANGE: `SelectMenuBuilder` and `SelectMenuOptionBuilder` have been removed (prev. deprecated) BREAKING CHANGE: `EmbedBuilder` no longer takes camalCase options BREAKING CHANGE: `ActionRowBuilder` now has specialized `[add/set]X` methods as opposed to the current `[add/set]Components` BREAKING CHANGE: Removed `equals` methods BREAKING CHANGE: Sapphire -> zod for validation BREAKING CHANGE: Removed the ability to pass `null`/`undefined` to clear fields, use `clearX()` instead BREAKING CHANGE: Renamed all "slash command" symbols to instead use "chat input command" BREAKING CHANGE: Removed `ContextMenuCommandBuilder` in favor of `MessageCommandBuilder` and `UserCommandBuilder` BREAKING CHANGE: Removed support for passing the "string key"s of enums BREAKING CHANGE: Removed `Button` class in favor for specialized classes depending on the style BREAKING CHANGE: Removed nested `addX` styled-methods in favor of plural `addXs` Co-authored-by: Vlad Frangu <me@vladfrangu.dev> Co-authored-by: Almeida <github@almeidx.dev>
This commit is contained in:
@@ -1,68 +1,60 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import {
|
||||
type APIActionRowComponent,
|
||||
ComponentType,
|
||||
type APIMessageActionRowComponent,
|
||||
type APIModalActionRowComponent,
|
||||
type APIActionRowComponentTypes,
|
||||
import type {
|
||||
APITextInputComponent,
|
||||
APIActionRowComponent,
|
||||
APIActionRowComponentTypes,
|
||||
APIChannelSelectComponent,
|
||||
APIMentionableSelectComponent,
|
||||
APIRoleSelectComponent,
|
||||
APIStringSelectComponent,
|
||||
APIUserSelectComponent,
|
||||
APIButtonComponentWithCustomId,
|
||||
APIButtonComponentWithSKUId,
|
||||
APIButtonComponentWithURL,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
|
||||
import { resolveBuilder } from '../util/resolveBuilder.js';
|
||||
import { isValidationEnabled } from '../util/validation.js';
|
||||
import { actionRowPredicate } from './Assertions.js';
|
||||
import { ComponentBuilder } from './Component.js';
|
||||
import type { AnyActionRowComponentBuilder } from './Components.js';
|
||||
import { createComponentBuilder } from './Components.js';
|
||||
import type { ButtonBuilder } from './button/Button.js';
|
||||
import type { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
|
||||
import type { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
|
||||
import type { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
|
||||
import type { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
|
||||
import type { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
||||
import type { TextInputBuilder } from './textInput/TextInput.js';
|
||||
import {
|
||||
DangerButtonBuilder,
|
||||
PrimaryButtonBuilder,
|
||||
SecondaryButtonBuilder,
|
||||
SuccessButtonBuilder,
|
||||
} from './button/CustomIdButton.js';
|
||||
import { LinkButtonBuilder } from './button/LinkButton.js';
|
||||
import { PremiumButtonBuilder } from './button/PremiumButton.js';
|
||||
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
|
||||
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
|
||||
import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
|
||||
import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
|
||||
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
||||
import { TextInputBuilder } from './textInput/TextInput.js';
|
||||
|
||||
/**
|
||||
* The builders that may be used for messages.
|
||||
*/
|
||||
export type MessageComponentBuilder =
|
||||
| ActionRowBuilder<MessageActionRowComponentBuilder>
|
||||
| MessageActionRowComponentBuilder;
|
||||
|
||||
/**
|
||||
* The builders that may be used for modals.
|
||||
*/
|
||||
export type ModalComponentBuilder = ActionRowBuilder<ModalActionRowComponentBuilder> | ModalActionRowComponentBuilder;
|
||||
|
||||
/**
|
||||
* The builders that may be used within an action row for messages.
|
||||
*/
|
||||
export type MessageActionRowComponentBuilder =
|
||||
| ButtonBuilder
|
||||
| ChannelSelectMenuBuilder
|
||||
| MentionableSelectMenuBuilder
|
||||
| RoleSelectMenuBuilder
|
||||
| StringSelectMenuBuilder
|
||||
| UserSelectMenuBuilder;
|
||||
|
||||
/**
|
||||
* The builders that may be used within an action row for modals.
|
||||
*/
|
||||
export type ModalActionRowComponentBuilder = TextInputBuilder;
|
||||
|
||||
/**
|
||||
* Any builder.
|
||||
*/
|
||||
export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;
|
||||
export interface ActionRowBuilderData
|
||||
extends Partial<Omit<APIActionRowComponent<APIActionRowComponentTypes>, 'components'>> {
|
||||
components: AnyActionRowComponentBuilder[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for action rows.
|
||||
*
|
||||
* @typeParam ComponentType - The types of components this action row holds
|
||||
*/
|
||||
export class ActionRowBuilder<ComponentType extends AnyComponentBuilder> extends ComponentBuilder<
|
||||
APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
|
||||
> {
|
||||
export class ActionRowBuilder extends ComponentBuilder<APIActionRowComponent<APIActionRowComponentTypes>> {
|
||||
private readonly data: ActionRowBuilderData;
|
||||
|
||||
/**
|
||||
* The components within this action row.
|
||||
*/
|
||||
public readonly components: ComponentType[];
|
||||
public get components(): readonly AnyActionRowComponentBuilder[] {
|
||||
return this.data.components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new action row from API data.
|
||||
@@ -98,38 +90,256 @@ export class ActionRowBuilder<ComponentType extends AnyComponentBuilder> extends
|
||||
* .addComponents(button2, button3);
|
||||
* ```
|
||||
*/
|
||||
public constructor({ components, ...data }: Partial<APIActionRowComponent<APIActionRowComponentTypes>> = {}) {
|
||||
super({ type: ComponentType.ActionRow, ...data });
|
||||
this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentType[];
|
||||
public constructor({ components = [], ...data }: Partial<APIActionRowComponent<APIActionRowComponentTypes>> = {}) {
|
||||
super();
|
||||
this.data = {
|
||||
...structuredClone(data),
|
||||
type: ComponentType.ActionRow,
|
||||
components: components.map((component) => createComponentBuilder(component)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds components to this action row.
|
||||
* Adds primary button components to this action row.
|
||||
*
|
||||
* @param components - The components to add
|
||||
* @param input - The buttons to add
|
||||
*/
|
||||
public addComponents(...components: RestOrArray<ComponentType>) {
|
||||
this.components.push(...normalizeArray(components));
|
||||
public addPrimaryButtonComponents(
|
||||
...input: RestOrArray<
|
||||
APIButtonComponentWithCustomId | PrimaryButtonBuilder | ((builder: PrimaryButtonBuilder) => PrimaryButtonBuilder)
|
||||
>
|
||||
): this {
|
||||
const normalized = normalizeArray(input);
|
||||
const resolved = normalized.map((component) => resolveBuilder(component, PrimaryButtonBuilder));
|
||||
|
||||
this.data.components.push(...resolved);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets components for this action row.
|
||||
* Adds secondary button components to this action row.
|
||||
*
|
||||
* @param components - The components to set
|
||||
* @param input - The buttons to add
|
||||
*/
|
||||
public setComponents(...components: RestOrArray<ComponentType>) {
|
||||
this.components.splice(0, this.components.length, ...normalizeArray(components));
|
||||
public addSecondaryButtonComponents(
|
||||
...input: RestOrArray<
|
||||
| APIButtonComponentWithCustomId
|
||||
| SecondaryButtonBuilder
|
||||
| ((builder: SecondaryButtonBuilder) => SecondaryButtonBuilder)
|
||||
>
|
||||
): this {
|
||||
const normalized = normalizeArray(input);
|
||||
const resolved = normalized.map((component) => resolveBuilder(component, SecondaryButtonBuilder));
|
||||
|
||||
this.data.components.push(...resolved);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds success button components to this action row.
|
||||
*
|
||||
* @param input - The buttons to add
|
||||
*/
|
||||
public addSuccessButtonComponents(
|
||||
...input: RestOrArray<
|
||||
APIButtonComponentWithCustomId | SuccessButtonBuilder | ((builder: SuccessButtonBuilder) => SuccessButtonBuilder)
|
||||
>
|
||||
): this {
|
||||
const normalized = normalizeArray(input);
|
||||
const resolved = normalized.map((component) => resolveBuilder(component, SuccessButtonBuilder));
|
||||
|
||||
this.data.components.push(...resolved);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds danger button components to this action row.
|
||||
*/
|
||||
public addDangerButtonComponents(
|
||||
...input: RestOrArray<
|
||||
APIButtonComponentWithCustomId | DangerButtonBuilder | ((builder: DangerButtonBuilder) => DangerButtonBuilder)
|
||||
>
|
||||
): this {
|
||||
const normalized = normalizeArray(input);
|
||||
const resolved = normalized.map((component) => resolveBuilder(component, DangerButtonBuilder));
|
||||
|
||||
this.data.components.push(...resolved);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generically add any type of component to this action row, only takes in an instance of a component builder.
|
||||
*/
|
||||
public addComponents(...input: RestOrArray<AnyActionRowComponentBuilder>): this {
|
||||
const normalized = normalizeArray(input);
|
||||
this.data.components.push(...normalized);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds SKU id button components to this action row.
|
||||
*
|
||||
* @param input - The buttons to add
|
||||
*/
|
||||
public addPremiumButtonComponents(
|
||||
...input: RestOrArray<
|
||||
APIButtonComponentWithSKUId | PremiumButtonBuilder | ((builder: PremiumButtonBuilder) => PremiumButtonBuilder)
|
||||
>
|
||||
): this {
|
||||
const normalized = normalizeArray(input);
|
||||
const resolved = normalized.map((component) => resolveBuilder(component, PremiumButtonBuilder));
|
||||
|
||||
this.data.components.push(...resolved);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds URL button components to this action row.
|
||||
*
|
||||
* @param input - The buttons to add
|
||||
*/
|
||||
public addLinkButtonComponents(
|
||||
...input: RestOrArray<
|
||||
APIButtonComponentWithURL | LinkButtonBuilder | ((builder: LinkButtonBuilder) => LinkButtonBuilder)
|
||||
>
|
||||
): this {
|
||||
const normalized = normalizeArray(input);
|
||||
const resolved = normalized.map((component) => resolveBuilder(component, LinkButtonBuilder));
|
||||
|
||||
this.data.components.push(...resolved);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a channel select menu component to this action row.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public addChannelSelectMenuComponent(
|
||||
input:
|
||||
| APIChannelSelectComponent
|
||||
| ChannelSelectMenuBuilder
|
||||
| ((builder: ChannelSelectMenuBuilder) => ChannelSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.components.push(resolveBuilder(input, ChannelSelectMenuBuilder));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mentionable select menu component to this action row.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public addMentionableSelectMenuComponent(
|
||||
input:
|
||||
| APIMentionableSelectComponent
|
||||
| MentionableSelectMenuBuilder
|
||||
| ((builder: MentionableSelectMenuBuilder) => MentionableSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.components.push(resolveBuilder(input, MentionableSelectMenuBuilder));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a role select menu component to this action row.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public addRoleSelectMenuComponent(
|
||||
input: APIRoleSelectComponent | RoleSelectMenuBuilder | ((builder: RoleSelectMenuBuilder) => RoleSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.components.push(resolveBuilder(input, RoleSelectMenuBuilder));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a string select menu component to this action row.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public addStringSelectMenuComponent(
|
||||
input:
|
||||
| APIStringSelectComponent
|
||||
| StringSelectMenuBuilder
|
||||
| ((builder: StringSelectMenuBuilder) => StringSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.components.push(resolveBuilder(input, StringSelectMenuBuilder));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a user select menu component to this action row.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public addUserSelectMenuComponent(
|
||||
input: APIUserSelectComponent | UserSelectMenuBuilder | ((builder: UserSelectMenuBuilder) => UserSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.components.push(resolveBuilder(input, UserSelectMenuBuilder));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a text input component to this action row.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public addTextInputComponent(
|
||||
input: APITextInputComponent | TextInputBuilder | ((builder: TextInputBuilder) => TextInputBuilder),
|
||||
): this {
|
||||
this.data.components.push(resolveBuilder(input, TextInputBuilder));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts components for this action row.
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing components of an action row.
|
||||
* @example
|
||||
* Remove the first component:
|
||||
* ```ts
|
||||
* actionRow.spliceComponents(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n components:
|
||||
* ```ts
|
||||
* const n = 4;
|
||||
* actionRow.spliceComponents(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last component:
|
||||
* ```ts
|
||||
* actionRow.spliceComponents(-1, 1);
|
||||
* ```
|
||||
* @param index - The index to start at
|
||||
* @param deleteCount - The number of components to remove
|
||||
* @param components - The replacing component objects
|
||||
*/
|
||||
public spliceComponents(index: number, deleteCount: number, ...components: AnyActionRowComponentBuilder[]): this {
|
||||
this.data.components.splice(index, deleteCount, ...components);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APIActionRowComponent<ReturnType<ComponentType['toJSON']>> {
|
||||
return {
|
||||
...this.data,
|
||||
components: this.components.map((component) => component.toJSON()),
|
||||
} as APIActionRowComponent<ReturnType<ComponentType['toJSON']>>;
|
||||
public override toJSON(validationOverride?: boolean): APIActionRowComponent<APIActionRowComponentTypes> {
|
||||
const { components, ...rest } = this.data;
|
||||
|
||||
const data = {
|
||||
...structuredClone(rest),
|
||||
components: components.map((component) => component.toJSON(validationOverride)),
|
||||
};
|
||||
|
||||
if (validationOverride ?? isValidationEnabled()) {
|
||||
actionRowPredicate.parse(data);
|
||||
}
|
||||
|
||||
return data as APIActionRowComponent<APIActionRowComponentTypes>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,127 +1,168 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord-api-types/v10';
|
||||
import { isValidationEnabled } from '../util/validation.js';
|
||||
import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js';
|
||||
import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { customIdPredicate, refineURLPredicate } from '../Assertions.js';
|
||||
|
||||
export const customIdValidator = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(100)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
const labelPredicate = z.string().min(1).max(80);
|
||||
|
||||
export const emojiValidator = s
|
||||
export const emojiPredicate = z
|
||||
.object({
|
||||
id: s.string(),
|
||||
name: s.string(),
|
||||
animated: s.boolean(),
|
||||
id: z.string().optional(),
|
||||
name: z.string().min(2).max(32).optional(),
|
||||
animated: z.boolean().optional(),
|
||||
})
|
||||
.partial()
|
||||
.strict()
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
.refine((data) => data.id !== undefined || data.name !== undefined, {
|
||||
message: "Either 'id' or 'name' must be provided",
|
||||
});
|
||||
|
||||
export const disabledValidator = s.boolean();
|
||||
const buttonPredicateBase = z.object({
|
||||
type: z.literal(ComponentType.Button),
|
||||
disabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const buttonLabelValidator = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(80)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
const buttonCustomIdPredicateBase = buttonPredicateBase.extend({
|
||||
custom_id: customIdPredicate,
|
||||
emoji: emojiPredicate.optional(),
|
||||
label: labelPredicate,
|
||||
});
|
||||
|
||||
export const buttonStyleValidator = s.nativeEnum(ButtonStyle);
|
||||
const buttonPrimaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Primary) }).strict();
|
||||
const buttonSecondaryPredicate = buttonCustomIdPredicateBase
|
||||
.extend({ style: z.literal(ButtonStyle.Secondary) })
|
||||
.strict();
|
||||
const buttonSuccessPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Success) }).strict();
|
||||
const buttonDangerPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Danger) }).strict();
|
||||
|
||||
export const placeholderValidator = s.string().lengthLessThanOrEqual(150).setValidationEnabled(isValidationEnabled);
|
||||
export const minMaxValidator = s
|
||||
.number()
|
||||
.int()
|
||||
.greaterThanOrEqual(0)
|
||||
.lessThanOrEqual(25)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const labelValueDescriptionValidator = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(100)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const jsonOptionValidator = s
|
||||
.object({
|
||||
label: labelValueDescriptionValidator,
|
||||
value: labelValueDescriptionValidator,
|
||||
description: labelValueDescriptionValidator.optional(),
|
||||
emoji: emojiValidator.optional(),
|
||||
default: s.boolean().optional(),
|
||||
const buttonLinkPredicate = buttonPredicateBase
|
||||
.extend({
|
||||
style: z.literal(ButtonStyle.Link),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.refine(refineURLPredicate(['http:', 'https:', 'discord:'])),
|
||||
emoji: emojiPredicate.optional(),
|
||||
label: labelPredicate,
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
.strict();
|
||||
|
||||
export const optionValidator = s.instance(StringSelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const optionsValidator = optionValidator
|
||||
.array()
|
||||
.lengthGreaterThanOrEqual(0)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
export const optionsLengthValidator = s
|
||||
.number()
|
||||
.int()
|
||||
.greaterThanOrEqual(0)
|
||||
.lessThanOrEqual(25)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export function validateRequiredSelectMenuParameters(options: StringSelectMenuOptionBuilder[], customId?: string) {
|
||||
customIdValidator.parse(customId);
|
||||
optionsValidator.parse(options);
|
||||
}
|
||||
|
||||
export const defaultValidator = s.boolean();
|
||||
|
||||
export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) {
|
||||
labelValueDescriptionValidator.parse(label);
|
||||
labelValueDescriptionValidator.parse(value);
|
||||
}
|
||||
|
||||
export const channelTypesValidator = s.nativeEnum(ChannelType).array().setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const urlValidator = s
|
||||
.string()
|
||||
.url({
|
||||
allowedProtocols: ['http:', 'https:', 'discord:'],
|
||||
const buttonPremiumPredicate = buttonPredicateBase
|
||||
.extend({
|
||||
style: z.literal(ButtonStyle.Premium),
|
||||
sku_id: z.string(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
.strict();
|
||||
|
||||
export function validateRequiredButtonParameters(
|
||||
style?: ButtonStyle,
|
||||
label?: string,
|
||||
emoji?: APIMessageComponentEmoji,
|
||||
customId?: string,
|
||||
skuId?: string,
|
||||
url?: string,
|
||||
) {
|
||||
if (style === ButtonStyle.Premium) {
|
||||
if (!skuId) {
|
||||
throw new RangeError('Premium buttons must have an SKU id.');
|
||||
export const buttonPredicate = z.discriminatedUnion('style', [
|
||||
buttonLinkPredicate,
|
||||
buttonPrimaryPredicate,
|
||||
buttonSecondaryPredicate,
|
||||
buttonSuccessPredicate,
|
||||
buttonDangerPredicate,
|
||||
buttonPremiumPredicate,
|
||||
]);
|
||||
|
||||
const selectMenuBasePredicate = z.object({
|
||||
placeholder: z.string().max(150).optional(),
|
||||
min_values: z.number().min(0).max(25).optional(),
|
||||
max_values: z.number().min(0).max(25).optional(),
|
||||
custom_id: customIdPredicate,
|
||||
disabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const selectMenuChannelPredicate = selectMenuBasePredicate.extend({
|
||||
type: z.literal(ComponentType.ChannelSelect),
|
||||
channel_types: z.nativeEnum(ChannelType).array().optional(),
|
||||
default_values: z
|
||||
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Channel) })
|
||||
.array()
|
||||
.max(25)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({
|
||||
type: z.literal(ComponentType.MentionableSelect),
|
||||
default_values: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
type: z.union([z.literal(SelectMenuDefaultValueType.Role), z.literal(SelectMenuDefaultValueType.User)]),
|
||||
})
|
||||
.array()
|
||||
.max(25)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const selectMenuRolePredicate = selectMenuBasePredicate.extend({
|
||||
type: z.literal(ComponentType.RoleSelect),
|
||||
default_values: z
|
||||
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Role) })
|
||||
.array()
|
||||
.max(25)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const selectMenuStringOptionPredicate = z.object({
|
||||
label: labelPredicate,
|
||||
value: z.string().min(1).max(100),
|
||||
description: z.string().min(1).max(100).optional(),
|
||||
emoji: emojiPredicate.optional(),
|
||||
default: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const selectMenuStringPredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: z.literal(ComponentType.StringSelect),
|
||||
options: selectMenuStringOptionPredicate.array().min(1).max(25),
|
||||
})
|
||||
.superRefine((menu, ctx) => {
|
||||
const addIssue = (name: string, minimum: number) =>
|
||||
ctx.addIssue({
|
||||
code: 'too_small',
|
||||
message: `The number of options must be greater than or equal to ${name}`,
|
||||
inclusive: true,
|
||||
minimum,
|
||||
type: 'number',
|
||||
path: ['options'],
|
||||
});
|
||||
|
||||
if (menu.max_values !== undefined && menu.options.length < menu.max_values) {
|
||||
addIssue('max_values', menu.max_values);
|
||||
}
|
||||
|
||||
if (customId || label || url || emoji) {
|
||||
throw new RangeError('Premium buttons cannot have a custom id, label, URL, or emoji.');
|
||||
}
|
||||
} else {
|
||||
if (skuId) {
|
||||
throw new RangeError('Non-premium buttons must not have an SKU id.');
|
||||
if (menu.min_values !== undefined && menu.options.length < menu.min_values) {
|
||||
addIssue('min_values', menu.min_values);
|
||||
}
|
||||
});
|
||||
|
||||
if (url && customId) {
|
||||
throw new RangeError('URL and custom id are mutually exclusive.');
|
||||
}
|
||||
export const selectMenuUserPredicate = selectMenuBasePredicate.extend({
|
||||
type: z.literal(ComponentType.UserSelect),
|
||||
default_values: z
|
||||
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.User) })
|
||||
.array()
|
||||
.max(25)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
if (!label && !emoji) {
|
||||
throw new RangeError('Non-premium buttons must have a label and/or an emoji.');
|
||||
}
|
||||
|
||||
if (style === ButtonStyle.Link) {
|
||||
if (!url) {
|
||||
throw new RangeError('Link buttons must have a URL.');
|
||||
}
|
||||
} else if (url) {
|
||||
throw new RangeError('Non-premium and non-link buttons cannot have a URL.');
|
||||
}
|
||||
}
|
||||
}
|
||||
export const actionRowPredicate = z.object({
|
||||
type: z.literal(ComponentType.ActionRow),
|
||||
components: z.union([
|
||||
z
|
||||
.object({ type: z.literal(ComponentType.Button) })
|
||||
.array()
|
||||
.min(1)
|
||||
.max(5),
|
||||
z
|
||||
.object({
|
||||
type: z.union([
|
||||
z.literal(ComponentType.ChannelSelect),
|
||||
z.literal(ComponentType.MentionableSelect),
|
||||
z.literal(ComponentType.RoleSelect),
|
||||
z.literal(ComponentType.StringSelect),
|
||||
z.literal(ComponentType.UserSelect),
|
||||
// And this!
|
||||
z.literal(ComponentType.TextInput),
|
||||
]),
|
||||
})
|
||||
.array()
|
||||
.length(1),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIActionRowComponentTypes,
|
||||
APIBaseComponent,
|
||||
ComponentType,
|
||||
} from 'discord-api-types/v10';
|
||||
import type { APIActionRowComponent, APIActionRowComponentTypes } from 'discord-api-types/v10';
|
||||
|
||||
/**
|
||||
* Any action row component data represented as an object.
|
||||
@@ -14,32 +9,15 @@ export type AnyAPIActionRowComponent = APIActionRowComponent<APIActionRowCompone
|
||||
/**
|
||||
* The base component builder that contains common symbols for all sorts of components.
|
||||
*
|
||||
* @typeParam DataType - The type of internal API data that is stored within the component
|
||||
* @typeParam Component - The type of API data that is stored within the builder
|
||||
*/
|
||||
export abstract class ComponentBuilder<
|
||||
DataType extends Partial<APIBaseComponent<ComponentType>> = APIBaseComponent<ComponentType>,
|
||||
> implements JSONEncodable<AnyAPIActionRowComponent>
|
||||
{
|
||||
/**
|
||||
* The API data associated with this component.
|
||||
*/
|
||||
public readonly data: Partial<DataType>;
|
||||
|
||||
export abstract class ComponentBuilder<Component extends AnyAPIActionRowComponent> implements JSONEncodable<Component> {
|
||||
/**
|
||||
* Serializes this builder to API-compatible JSON data.
|
||||
*
|
||||
* @remarks
|
||||
* This method runs validations on the data before serializing it.
|
||||
* As such, it may throw an error if the data is invalid.
|
||||
*/
|
||||
public abstract toJSON(): AnyAPIActionRowComponent;
|
||||
|
||||
/**
|
||||
* Constructs a new kind of component.
|
||||
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||
*
|
||||
* @param data - The data to construct a component out of
|
||||
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||
*/
|
||||
public constructor(data: Partial<DataType>) {
|
||||
this.data = data;
|
||||
}
|
||||
public abstract toJSON(validationOverride?: boolean): Component;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { ComponentType, type APIMessageComponent, type APIModalComponent } from 'discord-api-types/v10';
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
type AnyComponentBuilder,
|
||||
type MessageComponentBuilder,
|
||||
type ModalComponentBuilder,
|
||||
} from './ActionRow.js';
|
||||
import type { APIButtonComponent, APIMessageComponent, APIModalComponent } from 'discord-api-types/v10';
|
||||
import { ButtonStyle, ComponentType } from 'discord-api-types/v10';
|
||||
import { ActionRowBuilder } from './ActionRow.js';
|
||||
import type { AnyAPIActionRowComponent } from './Component.js';
|
||||
import { ComponentBuilder } from './Component.js';
|
||||
import { ButtonBuilder } from './button/Button.js';
|
||||
import type { BaseButtonBuilder } from './button/Button.js';
|
||||
import {
|
||||
DangerButtonBuilder,
|
||||
PrimaryButtonBuilder,
|
||||
SecondaryButtonBuilder,
|
||||
SuccessButtonBuilder,
|
||||
} from './button/CustomIdButton.js';
|
||||
import { LinkButtonBuilder } from './button/LinkButton.js';
|
||||
import { PremiumButtonBuilder } from './button/PremiumButton.js';
|
||||
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
|
||||
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
|
||||
import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
|
||||
@@ -14,6 +19,48 @@ import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
|
||||
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
||||
import { TextInputBuilder } from './textInput/TextInput.js';
|
||||
|
||||
/**
|
||||
* The builders that may be used for messages.
|
||||
*/
|
||||
export type MessageComponentBuilder = ActionRowBuilder | MessageActionRowComponentBuilder;
|
||||
|
||||
/**
|
||||
* The builders that may be used for modals.
|
||||
*/
|
||||
export type ModalComponentBuilder = ActionRowBuilder | ModalActionRowComponentBuilder;
|
||||
|
||||
/**
|
||||
* Any button builder
|
||||
*/
|
||||
export type ButtonBuilder =
|
||||
| DangerButtonBuilder
|
||||
| LinkButtonBuilder
|
||||
| PremiumButtonBuilder
|
||||
| PrimaryButtonBuilder
|
||||
| SecondaryButtonBuilder
|
||||
| SuccessButtonBuilder;
|
||||
|
||||
/**
|
||||
* The builders that may be used within an action row for messages.
|
||||
*/
|
||||
export type MessageActionRowComponentBuilder =
|
||||
| ButtonBuilder
|
||||
| ChannelSelectMenuBuilder
|
||||
| MentionableSelectMenuBuilder
|
||||
| RoleSelectMenuBuilder
|
||||
| StringSelectMenuBuilder
|
||||
| UserSelectMenuBuilder;
|
||||
|
||||
/**
|
||||
* The builders that may be used within an action row for modals.
|
||||
*/
|
||||
export type ModalActionRowComponentBuilder = TextInputBuilder;
|
||||
|
||||
/**
|
||||
* Any action row component builder.
|
||||
*/
|
||||
export type AnyActionRowComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;
|
||||
|
||||
/**
|
||||
* Components here are mapped to their respective builder.
|
||||
*/
|
||||
@@ -21,9 +68,9 @@ export interface MappedComponentTypes {
|
||||
/**
|
||||
* The action row component type is associated with an {@link ActionRowBuilder}.
|
||||
*/
|
||||
[ComponentType.ActionRow]: ActionRowBuilder<AnyComponentBuilder>;
|
||||
[ComponentType.ActionRow]: ActionRowBuilder;
|
||||
/**
|
||||
* The button component type is associated with a {@link ButtonBuilder}.
|
||||
* The button component type is associated with a {@link BaseButtonBuilder}.
|
||||
*/
|
||||
[ComponentType.Button]: ButtonBuilder;
|
||||
/**
|
||||
@@ -75,7 +122,7 @@ export function createComponentBuilder<ComponentBuilder extends MessageComponent
|
||||
|
||||
export function createComponentBuilder(
|
||||
data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
|
||||
): ComponentBuilder {
|
||||
): ComponentBuilder<AnyAPIActionRowComponent> {
|
||||
if (data instanceof ComponentBuilder) {
|
||||
return data;
|
||||
}
|
||||
@@ -84,7 +131,7 @@ export function createComponentBuilder(
|
||||
case ComponentType.ActionRow:
|
||||
return new ActionRowBuilder(data);
|
||||
case ComponentType.Button:
|
||||
return new ButtonBuilder(data);
|
||||
return createButtonBuilder(data);
|
||||
case ComponentType.StringSelect:
|
||||
return new StringSelectMenuBuilder(data);
|
||||
case ComponentType.TextInput:
|
||||
@@ -102,3 +149,23 @@ export function createComponentBuilder(
|
||||
throw new Error(`Cannot properly serialize component type: ${data.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function createButtonBuilder(data: APIButtonComponent): ButtonBuilder {
|
||||
switch (data.style) {
|
||||
case ButtonStyle.Primary:
|
||||
return new PrimaryButtonBuilder(data);
|
||||
case ButtonStyle.Secondary:
|
||||
return new SecondaryButtonBuilder(data);
|
||||
case ButtonStyle.Success:
|
||||
return new SuccessButtonBuilder(data);
|
||||
case ButtonStyle.Danger:
|
||||
return new DangerButtonBuilder(data);
|
||||
case ButtonStyle.Link:
|
||||
return new LinkButtonBuilder(data);
|
||||
case ButtonStyle.Premium:
|
||||
return new PremiumButtonBuilder(data);
|
||||
default:
|
||||
// @ts-expect-error This case can still occur if we get a newer unsupported button style
|
||||
throw new Error(`Cannot properly serialize button with style: ${data.style}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +1,13 @@
|
||||
import {
|
||||
ComponentType,
|
||||
type APIButtonComponent,
|
||||
type APIButtonComponentWithCustomId,
|
||||
type APIButtonComponentWithSKUId,
|
||||
type APIButtonComponentWithURL,
|
||||
type APIMessageComponentEmoji,
|
||||
type ButtonStyle,
|
||||
type Snowflake,
|
||||
} from 'discord-api-types/v10';
|
||||
import {
|
||||
buttonLabelValidator,
|
||||
buttonStyleValidator,
|
||||
customIdValidator,
|
||||
disabledValidator,
|
||||
emojiValidator,
|
||||
urlValidator,
|
||||
validateRequiredButtonParameters,
|
||||
} from '../Assertions.js';
|
||||
import type { APIButtonComponent } from 'discord-api-types/v10';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { buttonPredicate } from '../Assertions.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for buttons.
|
||||
*/
|
||||
export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
|
||||
/**
|
||||
* Creates a new button from API data.
|
||||
*
|
||||
* @param data - The API data to create this button with
|
||||
* @example
|
||||
* Creating a button from an API data object:
|
||||
* ```ts
|
||||
* const button = new ButtonBuilder({
|
||||
* custom_id: 'a cool button',
|
||||
* style: ButtonStyle.Primary,
|
||||
* label: 'Click Me',
|
||||
* emoji: {
|
||||
* name: 'smile',
|
||||
* id: '123456789012345678',
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a button using setters and API data:
|
||||
* ```ts
|
||||
* const button = new ButtonBuilder({
|
||||
* style: ButtonStyle.Secondary,
|
||||
* label: 'Click Me',
|
||||
* })
|
||||
* .setEmoji({ name: '🙂' })
|
||||
* .setCustomId('another cool button');
|
||||
* ```
|
||||
*/
|
||||
public constructor(data?: Partial<APIButtonComponent>) {
|
||||
super({ type: ComponentType.Button, ...data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style of this button.
|
||||
*
|
||||
* @param style - The style to use
|
||||
*/
|
||||
public setStyle(style: ButtonStyle) {
|
||||
this.data.style = buttonStyleValidator.parse(style);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL for this button.
|
||||
*
|
||||
* @remarks
|
||||
* This method is only available to buttons using the `Link` button style.
|
||||
* Only three types of URL schemes are currently supported: `https://`, `http://`, and `discord://`.
|
||||
* @param url - The URL to use
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
(this.data as APIButtonComponentWithURL).url = urlValidator.parse(url);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom id for this button.
|
||||
*
|
||||
* @remarks
|
||||
* This method is only applicable to buttons that are not using the `Link` button style.
|
||||
* @param customId - The custom id to use
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
(this.data as APIButtonComponentWithCustomId).custom_id = customIdValidator.parse(customId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the SKU id that represents a purchasable SKU for this button.
|
||||
*
|
||||
* @remarks Only available when using premium-style buttons.
|
||||
* @param skuId - The SKU id to use
|
||||
*/
|
||||
public setSKUId(skuId: Snowflake) {
|
||||
(this.data as APIButtonComponentWithSKUId).sku_id = skuId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the emoji to display on this button.
|
||||
*
|
||||
* @param emoji - The emoji to use
|
||||
*/
|
||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).emoji = emojiValidator.parse(emoji);
|
||||
return this;
|
||||
}
|
||||
export abstract class BaseButtonBuilder<ButtonData extends APIButtonComponent> extends ComponentBuilder<ButtonData> {
|
||||
protected declare readonly data: Partial<ButtonData>;
|
||||
|
||||
/**
|
||||
* Sets whether this button is disabled.
|
||||
@@ -117,35 +15,20 @@ export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
|
||||
* @param disabled - Whether to disable this button
|
||||
*/
|
||||
public setDisabled(disabled = true) {
|
||||
this.data.disabled = disabledValidator.parse(disabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this button.
|
||||
*
|
||||
* @param label - The label to use
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).label = buttonLabelValidator.parse(label);
|
||||
this.data.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APIButtonComponent {
|
||||
validateRequiredButtonParameters(
|
||||
this.data.style,
|
||||
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).label,
|
||||
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).emoji,
|
||||
(this.data as APIButtonComponentWithCustomId).custom_id,
|
||||
(this.data as APIButtonComponentWithSKUId).sku_id,
|
||||
(this.data as APIButtonComponentWithURL).url,
|
||||
);
|
||||
public override toJSON(validationOverride?: boolean): ButtonData {
|
||||
const clone = structuredClone(this.data);
|
||||
|
||||
return {
|
||||
...this.data,
|
||||
} as APIButtonComponent;
|
||||
if (validationOverride ?? isValidationEnabled()) {
|
||||
buttonPredicate.parse(clone);
|
||||
}
|
||||
|
||||
return clone as ButtonData;
|
||||
}
|
||||
}
|
||||
|
||||
69
packages/builders/src/components/button/CustomIdButton.ts
Normal file
69
packages/builders/src/components/button/CustomIdButton.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ButtonStyle, ComponentType, type APIButtonComponentWithCustomId } from 'discord-api-types/v10';
|
||||
import { Mixin } from 'ts-mixer';
|
||||
import { BaseButtonBuilder } from './Button.js';
|
||||
import { EmojiOrLabelButtonMixin } from './mixins/EmojiOrLabelButtonMixin.js';
|
||||
|
||||
export type CustomIdButtonStyle = APIButtonComponentWithCustomId['style'];
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for buttons with custom IDs.
|
||||
*/
|
||||
export abstract class CustomIdButtonBuilder extends Mixin(
|
||||
BaseButtonBuilder<APIButtonComponentWithCustomId>,
|
||||
EmojiOrLabelButtonMixin,
|
||||
) {
|
||||
protected override readonly data: Partial<APIButtonComponentWithCustomId>;
|
||||
|
||||
protected constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
|
||||
super();
|
||||
this.data = { ...structuredClone(data), type: ComponentType.Button };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom id for this button.
|
||||
*
|
||||
* @remarks
|
||||
* This method is only applicable to buttons that are not using the `Link` button style.
|
||||
* @param customId - The custom id to use
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
this.data.custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the primary style).
|
||||
*/
|
||||
export class PrimaryButtonBuilder extends CustomIdButtonBuilder {
|
||||
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
|
||||
super({ ...data, style: ButtonStyle.Primary });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the secondary style).
|
||||
*/
|
||||
export class SecondaryButtonBuilder extends CustomIdButtonBuilder {
|
||||
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
|
||||
super({ ...data, style: ButtonStyle.Secondary });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the success style).
|
||||
*/
|
||||
export class SuccessButtonBuilder extends CustomIdButtonBuilder {
|
||||
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
|
||||
super({ ...data, style: ButtonStyle.Success });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the danger style).
|
||||
*/
|
||||
export class DangerButtonBuilder extends CustomIdButtonBuilder {
|
||||
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
|
||||
super({ ...data, style: ButtonStyle.Danger });
|
||||
}
|
||||
}
|
||||
34
packages/builders/src/components/button/LinkButton.ts
Normal file
34
packages/builders/src/components/button/LinkButton.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
type APIButtonComponent,
|
||||
type APIButtonComponentWithURL,
|
||||
} from 'discord-api-types/v10';
|
||||
import { Mixin } from 'ts-mixer';
|
||||
import { BaseButtonBuilder } from './Button.js';
|
||||
import { EmojiOrLabelButtonMixin } from './mixins/EmojiOrLabelButtonMixin.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for buttons with links.
|
||||
*/
|
||||
export class LinkButtonBuilder extends Mixin(BaseButtonBuilder<APIButtonComponentWithURL>, EmojiOrLabelButtonMixin) {
|
||||
protected override readonly data: Partial<APIButtonComponentWithURL>;
|
||||
|
||||
public constructor(data: Partial<APIButtonComponent> = {}) {
|
||||
super();
|
||||
this.data = { ...structuredClone(data), type: ComponentType.Button, style: ButtonStyle.Link };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL for this button.
|
||||
*
|
||||
* @remarks
|
||||
* This method is only available to buttons using the `Link` button style.
|
||||
* Only three types of URL schemes are currently supported: `https://`, `http://`, and `discord://`.
|
||||
* @param url - The URL to use
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
this.data.url = url;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
26
packages/builders/src/components/button/PremiumButton.ts
Normal file
26
packages/builders/src/components/button/PremiumButton.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { APIButtonComponentWithSKUId, Snowflake } from 'discord-api-types/v10';
|
||||
import { ButtonStyle, ComponentType } from 'discord-api-types/v10';
|
||||
import { BaseButtonBuilder } from './Button.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for premium buttons.
|
||||
*/
|
||||
export class PremiumButtonBuilder extends BaseButtonBuilder<APIButtonComponentWithSKUId> {
|
||||
protected override readonly data: Partial<APIButtonComponentWithSKUId>;
|
||||
|
||||
public constructor(data: Partial<APIButtonComponentWithSKUId> = {}) {
|
||||
super();
|
||||
this.data = { ...structuredClone(data), type: ComponentType.Button, style: ButtonStyle.Premium };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the SKU id that represents a purchasable SKU for this button.
|
||||
*
|
||||
* @remarks Only available when using premium-style buttons.
|
||||
* @param skuId - The SKU id to use
|
||||
*/
|
||||
public setSKUId(skuId: Snowflake) {
|
||||
this.data.sku_id = skuId;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { APIButtonComponent, APIButtonComponentWithSKUId, APIMessageComponentEmoji } from 'discord-api-types/v10';
|
||||
|
||||
export interface EmojiOrLabelButtonData
|
||||
extends Pick<Exclude<APIButtonComponent, APIButtonComponentWithSKUId>, 'emoji' | 'label'> {}
|
||||
|
||||
export class EmojiOrLabelButtonMixin {
|
||||
protected declare readonly data: EmojiOrLabelButtonData;
|
||||
|
||||
/**
|
||||
* Sets the emoji to display on this button.
|
||||
*
|
||||
* @param emoji - The emoji to use
|
||||
*/
|
||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
this.data.emoji = emoji;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the emoji on this button.
|
||||
*/
|
||||
public clearEmoji() {
|
||||
this.data.emoji = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this button.
|
||||
*
|
||||
* @param label - The label to use
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the label on this button.
|
||||
*/
|
||||
public clearLabel() {
|
||||
this.data.label = undefined;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type { APISelectMenuComponent } from 'discord-api-types/v10';
|
||||
import { customIdValidator, disabledValidator, minMaxValidator, placeholderValidator } from '../Assertions.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
|
||||
/**
|
||||
@@ -7,16 +7,29 @@ import { ComponentBuilder } from '../Component.js';
|
||||
*
|
||||
* @typeParam SelectMenuType - The type of select menu this would be instantiated for.
|
||||
*/
|
||||
export abstract class BaseSelectMenuBuilder<
|
||||
SelectMenuType extends APISelectMenuComponent,
|
||||
> extends ComponentBuilder<SelectMenuType> {
|
||||
export abstract class BaseSelectMenuBuilder<Data extends APISelectMenuComponent>
|
||||
extends ComponentBuilder<Data>
|
||||
implements JSONEncodable<APISelectMenuComponent>
|
||||
{
|
||||
protected abstract readonly data: Partial<
|
||||
Pick<Data, 'custom_id' | 'disabled' | 'max_values' | 'min_values' | 'placeholder'>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Sets the placeholder for this select menu.
|
||||
*
|
||||
* @param placeholder - The placeholder to use
|
||||
*/
|
||||
public setPlaceholder(placeholder: string) {
|
||||
this.data.placeholder = placeholderValidator.parse(placeholder);
|
||||
this.data.placeholder = placeholder;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the placeholder for this select menu.
|
||||
*/
|
||||
public clearPlaceholder() {
|
||||
this.data.placeholder = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -26,7 +39,7 @@ export abstract class BaseSelectMenuBuilder<
|
||||
* @param minValues - The minimum values that must be selected
|
||||
*/
|
||||
public setMinValues(minValues: number) {
|
||||
this.data.min_values = minMaxValidator.parse(minValues);
|
||||
this.data.min_values = minValues;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -36,7 +49,7 @@ export abstract class BaseSelectMenuBuilder<
|
||||
* @param maxValues - The maximum values that must be selected
|
||||
*/
|
||||
public setMaxValues(maxValues: number) {
|
||||
this.data.max_values = minMaxValidator.parse(maxValues);
|
||||
this.data.max_values = maxValues;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -46,7 +59,7 @@ export abstract class BaseSelectMenuBuilder<
|
||||
* @param customId - The custom id to use
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
this.data.custom_id = customIdValidator.parse(customId);
|
||||
this.data.custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -56,17 +69,7 @@ export abstract class BaseSelectMenuBuilder<
|
||||
* @param disabled - Whether this select menu is disabled
|
||||
*/
|
||||
public setDisabled(disabled = true) {
|
||||
this.data.disabled = disabledValidator.parse(disabled);
|
||||
this.data.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): SelectMenuType {
|
||||
customIdValidator.parse(this.data.custom_id);
|
||||
return {
|
||||
...this.data,
|
||||
} as SelectMenuType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,16 @@ import {
|
||||
SelectMenuDefaultValueType,
|
||||
} from 'discord-api-types/v10';
|
||||
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
||||
import { channelTypesValidator, customIdValidator, optionsLengthValidator } from '../Assertions.js';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { selectMenuChannelPredicate } from '../Assertions.js';
|
||||
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for channel select menus.
|
||||
*/
|
||||
export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSelectComponent> {
|
||||
protected override readonly data: Partial<APIChannelSelectComponent>;
|
||||
|
||||
/**
|
||||
* Creates a new select menu from API data.
|
||||
*
|
||||
@@ -36,8 +39,9 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
||||
* .setMinValues(2);
|
||||
* ```
|
||||
*/
|
||||
public constructor(data?: Partial<APIChannelSelectComponent>) {
|
||||
super({ ...data, type: ComponentType.ChannelSelect });
|
||||
public constructor(data: Partial<APIChannelSelectComponent> = {}) {
|
||||
super();
|
||||
this.data = { ...structuredClone(data), type: ComponentType.ChannelSelect };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,7 +52,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
||||
public addChannelTypes(...types: RestOrArray<ChannelType>) {
|
||||
const normalizedTypes = normalizeArray(types);
|
||||
this.data.channel_types ??= [];
|
||||
this.data.channel_types.push(...channelTypesValidator.parse(normalizedTypes));
|
||||
this.data.channel_types.push(...normalizedTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -60,7 +64,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
||||
public setChannelTypes(...types: RestOrArray<ChannelType>) {
|
||||
const normalizedTypes = normalizeArray(types);
|
||||
this.data.channel_types ??= [];
|
||||
this.data.channel_types.splice(0, this.data.channel_types.length, ...channelTypesValidator.parse(normalizedTypes));
|
||||
this.data.channel_types.splice(0, this.data.channel_types.length, ...normalizedTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -71,7 +75,6 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
||||
*/
|
||||
public addDefaultChannels(...channels: RestOrArray<Snowflake>) {
|
||||
const normalizedValues = normalizeArray(channels);
|
||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
||||
this.data.default_values ??= [];
|
||||
|
||||
this.data.default_values.push(
|
||||
@@ -91,7 +94,6 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
||||
*/
|
||||
public setDefaultChannels(...channels: RestOrArray<Snowflake>) {
|
||||
const normalizedValues = normalizeArray(channels);
|
||||
optionsLengthValidator.parse(normalizedValues.length);
|
||||
|
||||
this.data.default_values = normalizedValues.map((id) => ({
|
||||
id,
|
||||
@@ -102,13 +104,15 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc BaseSelectMenuBuilder.toJSON}
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(): APIChannelSelectComponent {
|
||||
customIdValidator.parse(this.data.custom_id);
|
||||
public override toJSON(validationOverride?: boolean): APIChannelSelectComponent {
|
||||
const clone = structuredClone(this.data);
|
||||
|
||||
return {
|
||||
...this.data,
|
||||
} as APIChannelSelectComponent;
|
||||
if (validationOverride ?? isValidationEnabled()) {
|
||||
selectMenuChannelPredicate.parse(clone);
|
||||
}
|
||||
|
||||
return clone as APIChannelSelectComponent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,16 @@ import {
|
||||
SelectMenuDefaultValueType,
|
||||
} from 'discord-api-types/v10';
|
||||
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
||||
import { optionsLengthValidator } from '../Assertions.js';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { selectMenuMentionablePredicate } from '../Assertions.js';
|
||||
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for mentionable select menus.
|
||||
*/
|
||||
export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMentionableSelectComponent> {
|
||||
protected override readonly data: Partial<APIMentionableSelectComponent>;
|
||||
|
||||
/**
|
||||
* Creates a new select menu from API data.
|
||||
*
|
||||
@@ -35,8 +38,9 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
|
||||
* .setMinValues(1);
|
||||
* ```
|
||||
*/
|
||||
public constructor(data?: Partial<APIMentionableSelectComponent>) {
|
||||
super({ ...data, type: ComponentType.MentionableSelect });
|
||||
public constructor(data: Partial<APIMentionableSelectComponent> = {}) {
|
||||
super();
|
||||
this.data = { ...structuredClone(data), type: ComponentType.MentionableSelect };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +50,6 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
|
||||
*/
|
||||
public addDefaultRoles(...roles: RestOrArray<Snowflake>) {
|
||||
const normalizedValues = normalizeArray(roles);
|
||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
||||
this.data.default_values ??= [];
|
||||
|
||||
this.data.default_values.push(
|
||||
@@ -66,7 +69,6 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
|
||||
*/
|
||||
public addDefaultUsers(...users: RestOrArray<Snowflake>) {
|
||||
const normalizedValues = normalizeArray(users);
|
||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
||||
this.data.default_values ??= [];
|
||||
|
||||
this.data.default_values.push(
|
||||
@@ -91,7 +93,6 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
|
||||
>
|
||||
) {
|
||||
const normalizedValues = normalizeArray(values);
|
||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
||||
this.data.default_values ??= [];
|
||||
this.data.default_values.push(...normalizedValues);
|
||||
return this;
|
||||
@@ -109,8 +110,20 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
|
||||
>
|
||||
) {
|
||||
const normalizedValues = normalizeArray(values);
|
||||
optionsLengthValidator.parse(normalizedValues.length);
|
||||
this.data.default_values = normalizedValues;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(validationOverride?: boolean): APIMentionableSelectComponent {
|
||||
const clone = structuredClone(this.data);
|
||||
|
||||
if (validationOverride ?? isValidationEnabled()) {
|
||||
selectMenuMentionablePredicate.parse(clone);
|
||||
}
|
||||
|
||||
return clone as APIMentionableSelectComponent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@ import {
|
||||
SelectMenuDefaultValueType,
|
||||
} from 'discord-api-types/v10';
|
||||
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
||||
import { optionsLengthValidator } from '../Assertions.js';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { selectMenuRolePredicate } from '../Assertions.js';
|
||||
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for role select menus.
|
||||
*/
|
||||
export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectComponent> {
|
||||
protected override readonly data: Partial<APIRoleSelectComponent>;
|
||||
|
||||
/**
|
||||
* Creates a new select menu from API data.
|
||||
*
|
||||
@@ -34,8 +37,9 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
|
||||
* .setMinValues(1);
|
||||
* ```
|
||||
*/
|
||||
public constructor(data?: Partial<APIRoleSelectComponent>) {
|
||||
super({ ...data, type: ComponentType.RoleSelect });
|
||||
public constructor(data: Partial<APIRoleSelectComponent> = {}) {
|
||||
super();
|
||||
this.data = { ...structuredClone(data), type: ComponentType.RoleSelect };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +49,6 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
|
||||
*/
|
||||
public addDefaultRoles(...roles: RestOrArray<Snowflake>) {
|
||||
const normalizedValues = normalizeArray(roles);
|
||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
||||
this.data.default_values ??= [];
|
||||
|
||||
this.data.default_values.push(
|
||||
@@ -65,7 +68,6 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
|
||||
*/
|
||||
public setDefaultRoles(...roles: RestOrArray<Snowflake>) {
|
||||
const normalizedValues = normalizeArray(roles);
|
||||
optionsLengthValidator.parse(normalizedValues.length);
|
||||
|
||||
this.data.default_values = normalizedValues.map((id) => ({
|
||||
id,
|
||||
@@ -74,4 +76,17 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(validationOverride?: boolean): APIRoleSelectComponent {
|
||||
const clone = structuredClone(this.data);
|
||||
|
||||
if (validationOverride ?? isValidationEnabled()) {
|
||||
selectMenuRolePredicate.parse(clone);
|
||||
}
|
||||
|
||||
return clone as APIRoleSelectComponent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import type { APIStringSelectComponent, APISelectMenuOption } from 'discord-api-types/v10';
|
||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||
import { jsonOptionValidator, optionsLengthValidator, validateRequiredSelectMenuParameters } from '../Assertions.js';
|
||||
import { resolveBuilder } from '../../util/resolveBuilder.js';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { selectMenuStringPredicate } from '../Assertions.js';
|
||||
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
||||
import { StringSelectMenuOptionBuilder } from './StringSelectMenuOption.js';
|
||||
|
||||
export interface StringSelectMenuData extends Partial<Omit<APIStringSelectComponent, 'options'>> {
|
||||
options: StringSelectMenuOptionBuilder[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for string select menus.
|
||||
*/
|
||||
export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSelectComponent> {
|
||||
protected override readonly data: StringSelectMenuData;
|
||||
|
||||
/**
|
||||
* The options within this select menu.
|
||||
* The options for this select menu.
|
||||
*/
|
||||
public readonly options: StringSelectMenuOptionBuilder[];
|
||||
public get options(): readonly StringSelectMenuOptionBuilder[] {
|
||||
return this.data.options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new select menu from API data.
|
||||
@@ -45,10 +57,13 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public constructor(data?: Partial<APIStringSelectComponent>) {
|
||||
const { options, ...initData } = data ?? {};
|
||||
super({ ...initData, type: ComponentType.StringSelect });
|
||||
this.options = options?.map((option: APISelectMenuOption) => new StringSelectMenuOptionBuilder(option)) ?? [];
|
||||
public constructor({ options = [], ...data }: Partial<APIStringSelectComponent> = {}) {
|
||||
super();
|
||||
this.data = {
|
||||
...structuredClone(data),
|
||||
options: options.map((option) => new StringSelectMenuOptionBuilder(option)),
|
||||
type: ComponentType.StringSelect,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,16 +71,18 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
|
||||
*
|
||||
* @param options - The options to add
|
||||
*/
|
||||
public addOptions(...options: RestOrArray<APISelectMenuOption | StringSelectMenuOptionBuilder>) {
|
||||
public addOptions(
|
||||
...options: RestOrArray<
|
||||
| APISelectMenuOption
|
||||
| StringSelectMenuOptionBuilder
|
||||
| ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder)
|
||||
>
|
||||
) {
|
||||
const normalizedOptions = normalizeArray(options);
|
||||
optionsLengthValidator.parse(this.options.length + normalizedOptions.length);
|
||||
this.options.push(
|
||||
...normalizedOptions.map((normalizedOption) =>
|
||||
normalizedOption instanceof StringSelectMenuOptionBuilder
|
||||
? normalizedOption
|
||||
: new StringSelectMenuOptionBuilder(jsonOptionValidator.parse(normalizedOption)),
|
||||
),
|
||||
);
|
||||
const resolved = normalizedOptions.map((option) => resolveBuilder(option, StringSelectMenuOptionBuilder));
|
||||
|
||||
this.data.options.push(...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -74,8 +91,14 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
|
||||
*
|
||||
* @param options - The options to set
|
||||
*/
|
||||
public setOptions(...options: RestOrArray<APISelectMenuOption | StringSelectMenuOptionBuilder>) {
|
||||
return this.spliceOptions(0, this.options.length, ...options);
|
||||
public setOptions(
|
||||
...options: RestOrArray<
|
||||
| APISelectMenuOption
|
||||
| StringSelectMenuOptionBuilder
|
||||
| ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder)
|
||||
>
|
||||
) {
|
||||
return this.spliceOptions(0, this.options.length, ...normalizeArray(options));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,36 +131,35 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
|
||||
public spliceOptions(
|
||||
index: number,
|
||||
deleteCount: number,
|
||||
...options: RestOrArray<APISelectMenuOption | StringSelectMenuOptionBuilder>
|
||||
...options: (
|
||||
| APISelectMenuOption
|
||||
| StringSelectMenuOptionBuilder
|
||||
| ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder)
|
||||
)[]
|
||||
) {
|
||||
const normalizedOptions = normalizeArray(options);
|
||||
const resolved = options.map((option) => resolveBuilder(option, StringSelectMenuOptionBuilder));
|
||||
|
||||
const clone = [...this.options];
|
||||
this.data.options ??= [];
|
||||
this.data.options.splice(index, deleteCount, ...resolved);
|
||||
|
||||
clone.splice(
|
||||
index,
|
||||
deleteCount,
|
||||
...normalizedOptions.map((normalizedOption) =>
|
||||
normalizedOption instanceof StringSelectMenuOptionBuilder
|
||||
? normalizedOption
|
||||
: new StringSelectMenuOptionBuilder(jsonOptionValidator.parse(normalizedOption)),
|
||||
),
|
||||
);
|
||||
|
||||
optionsLengthValidator.parse(clone.length);
|
||||
this.options.splice(0, this.options.length, ...clone);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc BaseSelectMenuBuilder.toJSON}
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(): APIStringSelectComponent {
|
||||
validateRequiredSelectMenuParameters(this.options, this.data.custom_id);
|
||||
public override toJSON(validationOverride?: boolean): APIStringSelectComponent {
|
||||
const { options, ...rest } = this.data;
|
||||
const data = {
|
||||
...(structuredClone(rest) as APIStringSelectComponent),
|
||||
// selectMenuStringPredicate covers the validation of options
|
||||
options: options.map((option) => option.toJSON(false)),
|
||||
};
|
||||
|
||||
return {
|
||||
...this.data,
|
||||
options: this.options.map((option) => option.toJSON()),
|
||||
} as APIStringSelectComponent;
|
||||
if (validationOverride ?? isValidationEnabled()) {
|
||||
selectMenuStringPredicate.parse(data);
|
||||
}
|
||||
|
||||
return data as APIStringSelectComponent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10';
|
||||
import {
|
||||
defaultValidator,
|
||||
emojiValidator,
|
||||
labelValueDescriptionValidator,
|
||||
validateRequiredSelectMenuOptionParameters,
|
||||
} from '../Assertions.js';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { selectMenuStringOptionPredicate } from '../Assertions.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for string select menu options.
|
||||
*/
|
||||
export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMenuOption> {
|
||||
private readonly data: Partial<APISelectMenuOption>;
|
||||
|
||||
/**
|
||||
* Creates a new string select menu option from API data.
|
||||
*
|
||||
@@ -33,7 +31,9 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
||||
* .setLabel('woah');
|
||||
* ```
|
||||
*/
|
||||
public constructor(public data: Partial<APISelectMenuOption> = {}) {}
|
||||
public constructor(data: Partial<APISelectMenuOption> = {}) {
|
||||
this.data = structuredClone(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this option.
|
||||
@@ -41,7 +41,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
||||
* @param label - The label to use
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = labelValueDescriptionValidator.parse(label);
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
||||
* @param value - The value to use
|
||||
*/
|
||||
public setValue(value: string) {
|
||||
this.data.value = labelValueDescriptionValidator.parse(value);
|
||||
this.data.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,15 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
||||
* @param description - The description to use
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
this.data.description = labelValueDescriptionValidator.parse(description);
|
||||
this.data.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the description for this option.
|
||||
*/
|
||||
public clearDescription() {
|
||||
this.data.description = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -71,7 +79,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
||||
* @param isDefault - Whether this option is selected by default
|
||||
*/
|
||||
public setDefault(isDefault = true) {
|
||||
this.data.default = defaultValidator.parse(isDefault);
|
||||
this.data.default = isDefault;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -81,18 +89,28 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
||||
* @param emoji - The emoji to use
|
||||
*/
|
||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
this.data.emoji = emojiValidator.parse(emoji);
|
||||
this.data.emoji = emoji;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc BaseSelectMenuBuilder.toJSON}
|
||||
* Clears the emoji for this option.
|
||||
*/
|
||||
public toJSON(): APISelectMenuOption {
|
||||
validateRequiredSelectMenuOptionParameters(this.data.label, this.data.value);
|
||||
public clearEmoji() {
|
||||
this.data.emoji = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
return {
|
||||
...this.data,
|
||||
} as APISelectMenuOption;
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(validationOverride?: boolean): APISelectMenuOption {
|
||||
const clone = structuredClone(this.data);
|
||||
|
||||
if (validationOverride ?? isValidationEnabled()) {
|
||||
selectMenuStringOptionPredicate.parse(clone);
|
||||
}
|
||||
|
||||
return clone as APISelectMenuOption;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@ import {
|
||||
SelectMenuDefaultValueType,
|
||||
} from 'discord-api-types/v10';
|
||||
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
||||
import { optionsLengthValidator } from '../Assertions.js';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { selectMenuUserPredicate } from '../Assertions.js';
|
||||
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for user select menus.
|
||||
*/
|
||||
export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectComponent> {
|
||||
protected override readonly data: Partial<APIUserSelectComponent>;
|
||||
|
||||
/**
|
||||
* Creates a new select menu from API data.
|
||||
*
|
||||
@@ -34,8 +37,9 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
|
||||
* .setMinValues(1);
|
||||
* ```
|
||||
*/
|
||||
public constructor(data?: Partial<APIUserSelectComponent>) {
|
||||
super({ ...data, type: ComponentType.UserSelect });
|
||||
public constructor(data: Partial<APIUserSelectComponent> = {}) {
|
||||
super();
|
||||
this.data = { ...structuredClone(data), type: ComponentType.UserSelect };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,9 +49,8 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
|
||||
*/
|
||||
public addDefaultUsers(...users: RestOrArray<Snowflake>) {
|
||||
const normalizedValues = normalizeArray(users);
|
||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
||||
this.data.default_values ??= [];
|
||||
|
||||
this.data.default_values ??= [];
|
||||
this.data.default_values.push(
|
||||
...normalizedValues.map((id) => ({
|
||||
id,
|
||||
@@ -65,7 +68,6 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
|
||||
*/
|
||||
public setDefaultUsers(...users: RestOrArray<Snowflake>) {
|
||||
const normalizedValues = normalizeArray(users);
|
||||
optionsLengthValidator.parse(normalizedValues.length);
|
||||
|
||||
this.data.default_values = normalizedValues.map((id) => ({
|
||||
id,
|
||||
@@ -74,4 +76,17 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(validationOverride?: boolean): APIUserSelectComponent {
|
||||
const clone = structuredClone(this.data);
|
||||
|
||||
if (validationOverride ?? isValidationEnabled()) {
|
||||
selectMenuUserPredicate.parse(clone);
|
||||
}
|
||||
|
||||
return clone as APIUserSelectComponent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { TextInputStyle } from 'discord-api-types/v10';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { customIdValidator } from '../Assertions.js';
|
||||
import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { customIdPredicate } from '../../Assertions.js';
|
||||
|
||||
export const textInputStyleValidator = s.nativeEnum(TextInputStyle);
|
||||
export const minLengthValidator = s
|
||||
.number()
|
||||
.int()
|
||||
.greaterThanOrEqual(0)
|
||||
.lessThanOrEqual(4_000)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
export const maxLengthValidator = s
|
||||
.number()
|
||||
.int()
|
||||
.greaterThanOrEqual(1)
|
||||
.lessThanOrEqual(4_000)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
export const requiredValidator = s.boolean();
|
||||
export const valueValidator = s.string().lengthLessThanOrEqual(4_000).setValidationEnabled(isValidationEnabled);
|
||||
export const placeholderValidator = s.string().lengthLessThanOrEqual(100).setValidationEnabled(isValidationEnabled);
|
||||
export const labelValidator = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(45)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export function validateRequiredParameters(customId?: string, style?: TextInputStyle, label?: string) {
|
||||
customIdValidator.parse(customId);
|
||||
textInputStyleValidator.parse(style);
|
||||
labelValidator.parse(label);
|
||||
}
|
||||
export const textInputPredicate = z.object({
|
||||
type: z.literal(ComponentType.TextInput),
|
||||
custom_id: customIdPredicate,
|
||||
label: z.string().min(1).max(45),
|
||||
style: z.nativeEnum(TextInputStyle),
|
||||
min_length: z.number().min(0).max(4_000).optional(),
|
||||
max_length: z.number().min(1).max(4_000).optional(),
|
||||
placeholder: z.string().max(100).optional(),
|
||||
value: z.string().max(4_000).optional(),
|
||||
required: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import { isJSONEncodable, type Equatable, type JSONEncodable } from '@discordjs/util';
|
||||
import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { customIdValidator } from '../Assertions.js';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import {
|
||||
maxLengthValidator,
|
||||
minLengthValidator,
|
||||
placeholderValidator,
|
||||
requiredValidator,
|
||||
valueValidator,
|
||||
validateRequiredParameters,
|
||||
labelValidator,
|
||||
textInputStyleValidator,
|
||||
} from './Assertions.js';
|
||||
import { textInputPredicate } from './Assertions.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for text inputs.
|
||||
*/
|
||||
export class TextInputBuilder
|
||||
extends ComponentBuilder<APITextInputComponent>
|
||||
implements Equatable<APITextInputComponent | JSONEncodable<APITextInputComponent>>
|
||||
{
|
||||
export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
|
||||
private readonly data: Partial<APITextInputComponent>;
|
||||
|
||||
/**
|
||||
* Creates a new text input from API data.
|
||||
*
|
||||
@@ -44,8 +32,9 @@ export class TextInputBuilder
|
||||
* .setStyle(TextInputStyle.Paragraph);
|
||||
* ```
|
||||
*/
|
||||
public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) {
|
||||
super({ type: ComponentType.TextInput, ...data });
|
||||
public constructor(data: Partial<APITextInputComponent> = {}) {
|
||||
super();
|
||||
this.data = { ...structuredClone(data), type: ComponentType.TextInput };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +43,7 @@ export class TextInputBuilder
|
||||
* @param customId - The custom id to use
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
this.data.custom_id = customIdValidator.parse(customId);
|
||||
this.data.custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -64,7 +53,7 @@ export class TextInputBuilder
|
||||
* @param label - The label to use
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = labelValidator.parse(label);
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -74,7 +63,7 @@ export class TextInputBuilder
|
||||
* @param style - The style to use
|
||||
*/
|
||||
public setStyle(style: TextInputStyle) {
|
||||
this.data.style = textInputStyleValidator.parse(style);
|
||||
this.data.style = style;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -84,7 +73,15 @@ export class TextInputBuilder
|
||||
* @param minLength - The minimum length of text for this text input
|
||||
*/
|
||||
public setMinLength(minLength: number) {
|
||||
this.data.min_length = minLengthValidator.parse(minLength);
|
||||
this.data.min_length = minLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the minimum length of text for this text input.
|
||||
*/
|
||||
public clearMinLength() {
|
||||
this.data.min_length = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -94,7 +91,15 @@ export class TextInputBuilder
|
||||
* @param maxLength - The maximum length of text for this text input
|
||||
*/
|
||||
public setMaxLength(maxLength: number) {
|
||||
this.data.max_length = maxLengthValidator.parse(maxLength);
|
||||
this.data.max_length = maxLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the maximum length of text for this text input.
|
||||
*/
|
||||
public clearMaxLength() {
|
||||
this.data.max_length = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -104,7 +109,15 @@ export class TextInputBuilder
|
||||
* @param placeholder - The placeholder to use
|
||||
*/
|
||||
public setPlaceholder(placeholder: string) {
|
||||
this.data.placeholder = placeholderValidator.parse(placeholder);
|
||||
this.data.placeholder = placeholder;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the placeholder for this text input.
|
||||
*/
|
||||
public clearPlaceholder() {
|
||||
this.data.placeholder = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -114,7 +127,15 @@ export class TextInputBuilder
|
||||
* @param value - The value to use
|
||||
*/
|
||||
public setValue(value: string) {
|
||||
this.data.value = valueValidator.parse(value);
|
||||
this.data.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the value for this text input.
|
||||
*/
|
||||
public clearValue() {
|
||||
this.data.value = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -124,29 +145,20 @@ export class TextInputBuilder
|
||||
* @param required - Whether this text input is required
|
||||
*/
|
||||
public setRequired(required = true) {
|
||||
this.data.required = requiredValidator.parse(required);
|
||||
this.data.required = required;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APITextInputComponent {
|
||||
validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label);
|
||||
public toJSON(validationOverride?: boolean): APITextInputComponent {
|
||||
const clone = structuredClone(this.data);
|
||||
|
||||
return {
|
||||
...this.data,
|
||||
} as APITextInputComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this is equal to another structure.
|
||||
*/
|
||||
public equals(other: APITextInputComponent | JSONEncodable<APITextInputComponent>): boolean {
|
||||
if (isJSONEncodable(other)) {
|
||||
return isEqual(other.toJSON(), this.data);
|
||||
if (validationOverride ?? isValidationEnabled()) {
|
||||
textInputPredicate.parse(clone);
|
||||
}
|
||||
|
||||
return isEqual(other, this.data);
|
||||
return clone as APITextInputComponent;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user