From 49d50595e339aa7f3dac2349d25b806a149de857 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 12:21:59 +1000 Subject: [PATCH 1/9] chore: initial structure for SelectMenu, with typings --- src/structures/MessageSelectMenu.js | 66 +++++++++++++++++++++++++++++ src/util/Constants.js | 2 +- typings/index.d.ts | 37 ++++++++++++++-- 3 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 src/structures/MessageSelectMenu.js diff --git a/src/structures/MessageSelectMenu.js b/src/structures/MessageSelectMenu.js new file mode 100644 index 000000000000..01c65fbc8a34 --- /dev/null +++ b/src/structures/MessageSelectMenu.js @@ -0,0 +1,66 @@ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); + +class MessageSelectMenu extends BaseMessageComponent { + /** + * @typedef {BaseMessageComponentOptions} MessageSelectMenuOptions + * @property {?string} [customID] A unique string to be sent in the interaction when clicked + * @property {?string} [placeholder] Custom placeholder text to display when nothing is selected + * @property {?number} [minValues] The minimum number of selections required + * @property {?number} [maxValues] The maximum number of selections allowed + * @property {?MessageSelectOption[]} [options] Options for the select menu + */ + + /** + * @typedef {Object} MessageSelectOption + * @property {string} label The text to be displayed on this option + * @property {string} value The value to be sent for this option + * @property {?string} [description] Optional description to show for this option + * @property {?Emoji} [emoji] Emoji to display for this option + * @property {boolean} [default] Render this option as the default selection + */ + + /** + * @param {MessageSelectMenu|MessageSelectMenuOptions} [data={}] MessageSelectMenu to clone or raw data + */ + constructor(data = {}) { + super({ type: 'SELECT_MENU' }); + + this.setup(data); + } + + super(data) { + /** + * A unique string to be sent in the interaction when clicked + * @type {?string} + */ + this.customID = data.custom_id ?? data.customID ?? null; + + /** + * Custom placeholder text to display when nothing is selected + * @type {?string} + */ + this.placeholder = data.placeholder ?? null; + + /** + * The minimum number of selections required + * @type {?number} + */ + this.minValues = data.min_values ?? data.minValues ?? null; + + /** + * The maximum number of selections allowed + * @type {?number} + */ + this.maxValues = data.max_values ?? data.maxValues ?? null; + + /** + * Options for the select menu + * @type {MessageSelectOption[]} + */ + this.options = data.options ?? []; + } +} + +module.exports = MessageSelectMenu; diff --git a/src/util/Constants.js b/src/util/Constants.js index a97dba1391b8..f83ce83bee74 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -830,7 +830,7 @@ exports.InteractionResponseTypes = createEnum([ * BUTTON * @typedef {string} MessageComponentType */ -exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON']); +exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU']); /** * The style of a message button diff --git a/typings/index.d.ts b/typings/index.d.ts index 968e41f7b050..d9491ae3e7db 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -51,6 +51,7 @@ declare enum MessageButtonStyles { declare enum MessageComponentTypes { ACTION_ROW = 1, BUTTON = 2, + SELECT_MENU = 3, } declare enum NSFWLevels { @@ -1443,6 +1444,16 @@ declare module 'discord.js' { public toJSON(): object; } + class MessageSelectMenu extends BaseMessageComponent { + constructor(data?: MessageSelectMenu | MessageSelectMenuOptions); + public customID: string | null; + public maxValues: number | null; + public minValues: number | null; + public options: MessageSelectOption[]; + public placeholder: string | null; + public type: 'SELECT_MENU'; + } + export class NewsChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); public messages: MessageManager; @@ -3307,15 +3318,18 @@ declare module 'discord.js' { maxProcessed?: number; } - type MessageComponent = BaseMessageComponent | MessageActionRow | MessageButton; - + type MessageComponent = BaseMessageComponent | MessageActionRow | MessageButton | MessageSelectMenu; interface MessageComponentInteractionCollectorOptions extends CollectorOptions { max?: number; maxComponents?: number; maxUsers?: number; } - type MessageComponentOptions = BaseMessageComponentOptions | MessageActionRowOptions | MessageButtonOptions; + type MessageComponentOptions = + | BaseMessageComponentOptions + | MessageActionRowOptions + | MessageButtonOptions + | MessageSelectMenuOptions; type MessageComponentResolvable = MessageComponent | MessageComponentOptions; @@ -3441,6 +3455,23 @@ declare module 'discord.js' { type MessageResolvable = Message | Snowflake; + interface MessageSelectMenuOptions extends BaseMessageComponentOptions { + customID?: string; + maxValues?: number; + minValues?: number; + options?: MessageSelectOption[]; + placeholder?: string; + type: 'SELECT_MENU' | MessageComponentTypes.SELECT_MENU; + } + + interface MessageSelectOption { + default?: boolean; + description?: string; + emoji?: RawEmoji; + label: string; + value: string; + } + type MessageTarget = | Interaction | TextChannel From ef5407bb79ad8c1bbbbc8b44791379d15ba8fb3f Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 14:20:48 +1000 Subject: [PATCH 2/9] feat: set methods for MessageSelectMenu --- src/structures/MessageSelectMenu.js | 96 +++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/structures/MessageSelectMenu.js b/src/structures/MessageSelectMenu.js index 01c65fbc8a34..afdf1adda63e 100644 --- a/src/structures/MessageSelectMenu.js +++ b/src/structures/MessageSelectMenu.js @@ -1,6 +1,7 @@ 'use strict'; const BaseMessageComponent = require('./BaseMessageComponent'); +const Util = require('../util/Util'); class MessageSelectMenu extends BaseMessageComponent { /** @@ -61,6 +62,101 @@ class MessageSelectMenu extends BaseMessageComponent { */ this.options = data.options ?? []; } + + /** + * Sets the custom ID of this select menu + * @param {string} customID A unique string to be sent in the interaction when clicked + * @returns {MessageSelectMenu} + */ + setCustomID(customID) { + this.customID = Util.resolveString(customID); + return this; + } + + /** + * Sets the maximum number of selection allowed for this select menu + * @param {number} maxValues Number of selections to be allowed + * @returns {MessageSelectMenu} + */ + setMaxValues(maxValues) { + this.maxValues = maxValues; + return this; + } + + /** + * Sets the minimum number of selection required for this select menu + * @param {number} minValues Number of selections to be required + * @returns {MessageSelectMenu} + */ + setMinValues(minValues) { + this.minValues = minValues; + return this; + } + + /** + * Sets the placeholder of this select menu + * @param {string} placeholder Custom placeholder text to display when nothing is selected + * @returns {MessageSelectMenu} + */ + setPlaceholder(placeholder) { + this.placeholder = Util.resolveString(placeholder); + return this; + } + + /** + * Adds an option to the select menu (max 25) + * @param {MessageSelectOption} option The option to add + * @returns {MessageSelectMenu} + */ + addOption(option) { + return this.addOptions({ ...option }); + } + + /** + * Adds options to the select menu (max 5). + * @param {...(MessageSelectOption[]|MessageSelectOption[])} options The options to add + * @returns {MessageSelectMenu} + */ + addOptions(...options) { + this.options.push(...this.constructor.normalizeOptions(options)); + return this; + } + + /** + * Removes, replaces, and inserts options in the select menu (max 25). + * @param {number} index The index to start at + * @param {number} deleteCount The number of options to remove + * @param {...MessageSelectOption|MessageSelectOption[]} [options] The replacing option objects + * @returns {MessageSelectMenu} + */ + spliceFields(index, deleteCount, ...options) { + this.fields.splice(index, deleteCount, ...this.constructor.normalizeOptions(...options)); + return this; + } + + /** + * Normalizes option input and resolves strings. + * @param {MessageSelectOption} option The select menu option to normalize + * @returns {MessageSelectOption} + */ + static normalizeOption(option) { + let { label, value, description, emoji } = option; + + label = Util.resolveString(label); + value = Util.resolveString(value); + description = typeof description !== 'undefined' ? Util.resolveString(description) : undefined; + + return { label, value, description, emoji, default: option.default }; + } + + /** + * Normalizes option input and resolves strings. + * @param {...MessageSelectOption|MessageSelectOption[]} options The select menu options to normalize + * @returns {MessageSelectOption[]} + */ + static normalizeOptions(...options) { + return options.flat(2).map(option => this.normalizeOption(option)); + } } module.exports = MessageSelectMenu; From 37501dc6c6a8e7a696b785d29106d61534e8c01c Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 15:14:32 +1000 Subject: [PATCH 3/9] feat: toJSON method --- src/structures/MessageSelectMenu.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/structures/MessageSelectMenu.js b/src/structures/MessageSelectMenu.js index afdf1adda63e..7b2a7f43896f 100644 --- a/src/structures/MessageSelectMenu.js +++ b/src/structures/MessageSelectMenu.js @@ -134,6 +134,20 @@ class MessageSelectMenu extends BaseMessageComponent { return this; } + /** + * Transforms this select menu to a plain object + * @returns {Object} The raw data of this select menu + */ + toJSON() { + return { + custom_id: this.customID, + placeholder: this.placeholder, + min_values: this.minValues, + max_values: this.maxValues, + options: this.options, + }; + } + /** * Normalizes option input and resolves strings. * @param {MessageSelectOption} option The select menu option to normalize From b2872f56defb10efbc3b174b5d571ee64aabf5e8 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 15:41:25 +1000 Subject: [PATCH 4/9] fix: missing docs --- src/util/Constants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index f83ce83bee74..a617683fffb8 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -828,6 +828,7 @@ exports.InteractionResponseTypes = createEnum([ * The type of a message component * ACTION_ROW * BUTTON + * SELECT_MENU * @typedef {string} MessageComponentType */ exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU']); From 7516ef71dec14ba663ac9f8ba1794ad04eb65d1f Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 15:41:57 +1000 Subject: [PATCH 5/9] feat: support creating select menus --- src/structures/BaseMessageComponent.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index 65e055561c73..32f5b5fa7858 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -17,12 +17,12 @@ class BaseMessageComponent { * Data that can be resolved into options for a MessageComponent. This can be: * * MessageActionRowOptions * * MessageButtonOptions - * @typedef {MessageActionRowOptions|MessageButtonOptions} MessageComponentOptions + * @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions */ /** * Components that can be sent in a message - * @typedef {MessageActionRow|MessageButton} MessageComponent + * @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent */ /** @@ -87,6 +87,11 @@ class BaseMessageComponent { component = new MessageButton(data); break; } + case MessageComponentTypes.SELECT_MENU: { + const MessageSelectMenu = require('./MessageSelectMenu'); + component = new MessageSelectMenu(data); + break; + } default: if (client) { client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`); From b23725f0d9cefdf28f60993c91da1dc156d79536 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 16:51:54 +1000 Subject: [PATCH 6/9] chore: transform min/max correctly --- src/index.js | 1 + src/structures/MessageComponentInteraction.js | 6 ++++++ src/structures/MessageSelectMenu.js | 9 ++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index ad7193a10791..68dd4cac894d 100644 --- a/src/index.js +++ b/src/index.js @@ -101,6 +101,7 @@ module.exports = { MessageEmbed: require('./structures/MessageEmbed'), MessageMentions: require('./structures/MessageMentions'), MessageReaction: require('./structures/MessageReaction'), + MessageSelectMenu: require('./structures/MessageSelectMenu'), NewsChannel: require('./structures/NewsChannel'), PermissionOverwrites: require('./structures/PermissionOverwrites'), Presence: require('./structures/Presence').Presence, diff --git a/src/structures/MessageComponentInteraction.js b/src/structures/MessageComponentInteraction.js index 381019d2a4f8..42cbe331dffc 100644 --- a/src/structures/MessageComponentInteraction.js +++ b/src/structures/MessageComponentInteraction.js @@ -49,6 +49,12 @@ class MessageComponentInteraction extends Interaction { * @type {WebhookClient} */ this.webhook = new WebhookClient(this.applicationID, this.token, this.client.options); + + /** + * The values selected in a MessageSelectMenu interaction + * @type {string[]} + */ + this.values = this.componentType === 'SELECT_MENU' ? data.data.values : null; } /** diff --git a/src/structures/MessageSelectMenu.js b/src/structures/MessageSelectMenu.js index 7b2a7f43896f..3a7ad1561671 100644 --- a/src/structures/MessageSelectMenu.js +++ b/src/structures/MessageSelectMenu.js @@ -1,6 +1,7 @@ 'use strict'; const BaseMessageComponent = require('./BaseMessageComponent'); +const { MessageComponentTypes } = require('../util/Constants'); const Util = require('../util/Util'); class MessageSelectMenu extends BaseMessageComponent { @@ -31,7 +32,7 @@ class MessageSelectMenu extends BaseMessageComponent { this.setup(data); } - super(data) { + setup(data) { /** * A unique string to be sent in the interaction when clicked * @type {?string} @@ -84,7 +85,8 @@ class MessageSelectMenu extends BaseMessageComponent { } /** - * Sets the minimum number of selection required for this select menu + * Sets the minimum number of selections required for this select menu + * This will default the maxValues to the number of options, unless manually set * @param {number} minValues Number of selections to be required * @returns {MessageSelectMenu} */ @@ -143,8 +145,9 @@ class MessageSelectMenu extends BaseMessageComponent { custom_id: this.customID, placeholder: this.placeholder, min_values: this.minValues, - max_values: this.maxValues, + max_values: this.maxValues ?? this.minValues ? this.options.length : undefined, options: this.options, + type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type, }; } From 65d972aeffd72f4c8302957a5e9b8bcb9275fdf5 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 16:54:05 +1000 Subject: [PATCH 7/9] types: done --- typings/index.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index d9491ae3e7db..bdf433794501 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1301,10 +1301,12 @@ declare module 'discord.js' { } export class MessageComponentInteraction extends Interaction { + public componentType: MessageComponentType; public customID: string; public deferred: boolean; public message: Message | RawMessage; public replied: boolean; + public values: string[] | null; public webhook: WebhookClient; public defer(ephemeral?: boolean): Promise; public deferUpdate(): Promise; @@ -1452,6 +1454,13 @@ declare module 'discord.js' { public options: MessageSelectOption[]; public placeholder: string | null; public type: 'SELECT_MENU'; + public addOption(option: MessageSelectOption): this; + public addOptions(options: MessageSelectOption[] | MessageSelectOption[][]): this; + public setCustomID(customID: string): this; + public setMaxValues(maxValues: number): this; + public setMinValues(minValues: number): this; + public setPlaceholder(placeholder: string): this; + public toJSON(): object; } export class NewsChannel extends TextBasedChannel(GuildChannel) { From 0b21e72e75838913c7650a55a084a64f6d48104f Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 16:54:51 +1000 Subject: [PATCH 8/9] feat: esm export --- esm/discord.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/esm/discord.mjs b/esm/discord.mjs index 16c6281457c4..22259bd90fbc 100644 --- a/esm/discord.mjs +++ b/esm/discord.mjs @@ -89,6 +89,7 @@ export const { MessageEmbed, MessageMentions, MessageReaction, + MessageSelectMenu, NewsChannel, PermissionOverwrites, Presence, From f58d61433a370ed718fce8735455db1949b5f265 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 17:16:41 +1000 Subject: [PATCH 9/9] fix: emoji typings --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index bdf433794501..ea2bdb85bb58 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -3311,7 +3311,7 @@ declare module 'discord.js' { interface MessageButtonOptions extends BaseMessageComponentOptions { customID?: string; disabled?: boolean; - emoji?: RawEmoji; + emoji?: GuildEmoji | RawEmoji; label?: string; style: MessageButtonStyleResolvable; type: 'BUTTON' | MessageComponentTypes.BUTTON; @@ -3476,7 +3476,7 @@ declare module 'discord.js' { interface MessageSelectOption { default?: boolean; description?: string; - emoji?: RawEmoji; + emoji?: Emoji | RawEmoji; label: string; value: string; }