Skip to content

Commit e1cdcfa

Browse files
authoredApr 9, 2022
feat(modals): modals, input text components and modal submits, v13 style (#7431)
1 parent 5e8162a commit e1cdcfa

20 files changed

+836
-56
lines changed
 

‎package-lock.json

+21-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"@sapphire/async-queue": "^1.1.9",
5656
"@types/node-fetch": "^2.5.12",
5757
"@types/ws": "^8.2.2",
58-
"discord-api-types": "^0.26.0",
58+
"discord-api-types": "^0.27.1",
5959
"form-data": "^4.0.0",
6060
"node-fetch": "^2.6.1",
6161
"ws": "^8.4.0"

‎src/client/actions/InteractionCreate.js

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const AutocompleteInteraction = require('../../structures/AutocompleteInteractio
66
const ButtonInteraction = require('../../structures/ButtonInteraction');
77
const CommandInteraction = require('../../structures/CommandInteraction');
88
const MessageContextMenuInteraction = require('../../structures/MessageContextMenuInteraction');
9+
const ModalSubmitInteraction = require('../../structures/ModalSubmitInteraction');
910
const SelectMenuInteraction = require('../../structures/SelectMenuInteraction');
1011
const UserContextMenuInteraction = require('../../structures/UserContextMenuInteraction');
1112
const { Events, InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../../util/Constants');
@@ -59,6 +60,9 @@ class InteractionCreateAction extends Action {
5960
case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE:
6061
InteractionType = AutocompleteInteraction;
6162
break;
63+
case InteractionTypes.MODAL_SUBMIT:
64+
InteractionType = ModalSubmitInteraction;
65+
break;
6266
default:
6367
client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
6468
return;

‎src/errors/Messages.js

+12
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ const Messages = {
5858
SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string',
5959
SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string',
6060

61+
TEXT_INPUT_CUSTOM_ID: 'TextInputComponent customId must be a string',
62+
TEXT_INPUT_LABEL: 'TextInputComponent label must be a string',
63+
TEXT_INPUT_PLACEHOLDER: 'TextInputComponent placeholder must be a string',
64+
TEXT_INPUT_VALUE: 'TextInputComponent value must be a string',
65+
66+
MODAL_CUSTOM_ID: 'Modal customId must be a string',
67+
MODAL_TITLE: 'Modal title must be a string',
68+
6169
INTERACTION_COLLECTOR_ERROR: reason => `Collector received no interactions before ending with reason: ${reason}`,
6270

6371
FILE_NOT_FOUND: file => `File could not be found: ${file}`,
@@ -148,6 +156,10 @@ const Messages = {
148156
COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: 'No subcommand group specified for interaction.',
149157
AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION: 'No focused option for autocomplete interaction.',
150158

159+
MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND: customId => `Required field with custom id "${customId}" not found.`,
160+
MODAL_SUBMIT_INTERACTION_FIELD_TYPE: (customId, type, expected) =>
161+
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
162+
151163
INVITE_MISSING_SCOPES: 'At least one valid scope must be provided for the invite',
152164

153165
NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`,

‎src/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ exports.MessageMentions = require('./structures/MessageMentions');
122122
exports.MessagePayload = require('./structures/MessagePayload');
123123
exports.MessageReaction = require('./structures/MessageReaction');
124124
exports.MessageSelectMenu = require('./structures/MessageSelectMenu');
125+
exports.Modal = require('./structures/Modal');
126+
exports.ModalSubmitInteraction = require('./structures/ModalSubmitInteraction');
125127
exports.NewsChannel = require('./structures/NewsChannel');
126128
exports.OAuth2Guild = require('./structures/OAuth2Guild');
127129
exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel');
@@ -140,6 +142,7 @@ exports.StoreChannel = require('./structures/StoreChannel');
140142
exports.Team = require('./structures/Team');
141143
exports.TeamMember = require('./structures/TeamMember');
142144
exports.TextChannel = require('./structures/TextChannel');
145+
exports.TextInputComponent = require('./structures/TextInputComponent');
143146
exports.ThreadChannel = require('./structures/ThreadChannel');
144147
exports.ThreadMember = require('./structures/ThreadMember');
145148
exports.Typing = require('./structures/Typing');

‎src/structures/BaseCommandInteraction.js

+2
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ class BaseCommandInteraction extends Interaction {
196196
editReply() {}
197197
deleteReply() {}
198198
followUp() {}
199+
showModal() {}
200+
awaitModalSubmit() {}
199201
}
200202

201203
InteractionResponses.applyToClass(BaseCommandInteraction, ['deferUpdate', 'update']);

‎src/structures/BaseMessageComponent.js

+13-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { TypeError } = require('../errors');
44
const { MessageComponentTypes, Events } = require('../util/Constants');
55

66
/**
7-
* Represents an interactive component of a Message. It should not be necessary to construct this directly.
7+
* Represents an interactive component of a Message or Modal. It should not be necessary to construct this directly.
88
* See {@link MessageComponent}
99
*/
1010
class BaseMessageComponent {
@@ -15,18 +15,20 @@ class BaseMessageComponent {
1515
*/
1616

1717
/**
18-
* Data that can be resolved into options for a MessageComponent. This can be:
18+
* Data that can be resolved into options for a component. This can be:
1919
* * MessageActionRowOptions
2020
* * MessageButtonOptions
2121
* * MessageSelectMenuOptions
22+
* * TextInputComponentOptions
2223
* @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions
2324
*/
2425

2526
/**
26-
* Components that can be sent in a message. These can be:
27+
* Components that can be sent in a payload. These can be:
2728
* * MessageActionRow
2829
* * MessageButton
2930
* * MessageSelectMenu
31+
* * TextInputComponent
3032
* @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent
3133
* @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types}
3234
*/
@@ -51,10 +53,10 @@ class BaseMessageComponent {
5153
}
5254

5355
/**
54-
* Constructs a MessageComponent based on the type of the incoming data
56+
* Constructs a component based on the type of the incoming data
5557
* @param {MessageComponentOptions} data Data for a MessageComponent
5658
* @param {Client|WebhookClient} [client] Client constructing this component
57-
* @returns {?MessageComponent}
59+
* @returns {?(MessageComponent|ModalComponent)}
5860
* @private
5961
*/
6062
static create(data, client) {
@@ -79,6 +81,11 @@ class BaseMessageComponent {
7981
component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data);
8082
break;
8183
}
84+
case MessageComponentTypes.TEXT_INPUT: {
85+
const TextInputComponent = require('./TextInputComponent');
86+
component = data instanceof TextInputComponent ? data : new TextInputComponent(data);
87+
break;
88+
}
8289
default:
8390
if (client) {
8491
client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
@@ -90,7 +97,7 @@ class BaseMessageComponent {
9097
}
9198

9299
/**
93-
* Resolves the type of a MessageComponent
100+
* Resolves the type of a component
94101
* @param {MessageComponentTypeResolvable} type The type to resolve
95102
* @returns {MessageComponentType}
96103
* @private

‎src/structures/Interaction.js

+8
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ class Interaction extends Base {
173173
return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND && typeof this.targetId !== 'undefined';
174174
}
175175

176+
/**
177+
* Indicates whether this interaction is a {@link ModalSubmitInteraction}
178+
* @returns {boolean}
179+
*/
180+
isModalSubmit() {
181+
return InteractionTypes[this.type] === InteractionTypes.MODAL_SUBMIT;
182+
}
183+
176184
/**
177185
* Indicates whether this interaction is a {@link UserContextMenuInteraction}
178186
* @returns {boolean}

‎src/structures/MessageActionRow.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ class MessageActionRow extends BaseMessageComponent {
1212
* Components that can be placed in an action row
1313
* * MessageButton
1414
* * MessageSelectMenu
15-
* @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent
15+
* * TextInputComponent
16+
* @typedef {MessageButton|MessageSelectMenu|TextInputComponent} MessageActionRowComponent
1617
*/
1718

1819
/**
1920
* Options for components that can be placed in an action row
2021
* * MessageButtonOptions
2122
* * MessageSelectMenuOptions
22-
* @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions
23+
* * TextInputComponentOptions
24+
* @typedef {MessageButtonOptions|MessageSelectMenuOptions|TextInputComponentOptions} MessageActionRowComponentOptions
2325
*/
2426

2527
/**

‎src/structures/MessageComponentInteraction.js

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ class MessageComponentInteraction extends Interaction {
101101
followUp() {}
102102
deferUpdate() {}
103103
update() {}
104+
showModal() {}
105+
awaitModalSubmit() {}
104106
}
105107

106108
InteractionResponses.applyToClass(MessageComponentInteraction);

‎src/structures/Modal.js

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use strict';
2+
3+
const BaseMessageComponent = require('./BaseMessageComponent');
4+
const Util = require('../util/Util');
5+
6+
/**
7+
* Represents a modal (form) to be shown in response to an interaction
8+
*/
9+
class Modal {
10+
/**
11+
* @typedef {Object} ModalOptions
12+
* @property {string} [customId] A unique string to be sent in the interaction when clicked
13+
* @property {string} [title] The title to be displayed on this modal
14+
* @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
15+
* Action rows containing interactive components for the modal (text input components)
16+
*/
17+
18+
/**
19+
* @param {Modal|ModalOptions} data Modal to clone or raw data
20+
* @param {Client} client The client constructing this Modal, if provided
21+
*/
22+
constructor(data = {}, client = null) {
23+
/**
24+
* A list of MessageActionRows in the modal
25+
* @type {MessageActionRow[]}
26+
*/
27+
this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? [];
28+
29+
/**
30+
* A unique string to be sent in the interaction when submitted
31+
* @type {?string}
32+
*/
33+
this.customId = data.custom_id ?? data.customId ?? null;
34+
35+
/**
36+
* The title to be displayed on this modal
37+
* @type {?string}
38+
*/
39+
this.title = data.title ?? null;
40+
}
41+
42+
/**
43+
* Adds components to the modal.
44+
* @param {...MessageActionRowResolvable[]} components The components to add
45+
* @returns {Modal}
46+
*/
47+
addComponents(...components) {
48+
this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
49+
return this;
50+
}
51+
52+
/**
53+
* Sets the components of the modal.
54+
* @param {...MessageActionRowResolvable[]} components The components to set
55+
* @returns {Modal}
56+
*/
57+
setComponents(...components) {
58+
this.spliceComponents(0, this.components.length, components);
59+
return this;
60+
}
61+
62+
/**
63+
* Sets the custom id for this modal
64+
* @param {string} customId A unique string to be sent in the interaction when submitted
65+
* @returns {Modal}
66+
*/
67+
setCustomId(customId) {
68+
this.customId = Util.verifyString(customId, RangeError, 'MODAL_CUSTOM_ID');
69+
return this;
70+
}
71+
72+
/**
73+
* Removes, replaces, and inserts components in the modal.
74+
* @param {number} index The index to start at
75+
* @param {number} deleteCount The number of components to remove
76+
* @param {...MessageActionRowResolvable[]} [components] The replacing components
77+
* @returns {Modal}
78+
*/
79+
spliceComponents(index, deleteCount, ...components) {
80+
this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
81+
return this;
82+
}
83+
84+
/**
85+
* Sets the title of this modal
86+
* @param {string} title The title to be displayed on this modal
87+
* @returns {Modal}
88+
*/
89+
setTitle(title) {
90+
this.title = Util.verifyString(title, RangeError, 'MODAL_TITLE');
91+
return this;
92+
}
93+
94+
toJSON() {
95+
return {
96+
components: this.components.map(c => c.toJSON()),
97+
custom_id: this.customId,
98+
title: this.title,
99+
};
100+
}
101+
}
102+
103+
module.exports = Modal;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
3+
const { TypeError } = require('../errors');
4+
const { MessageComponentTypes } = require('../util/Constants');
5+
6+
/**
7+
* A resolver for modal submit interaction text inputs.
8+
*/
9+
class ModalSubmitFieldsResolver {
10+
constructor(components) {
11+
/**
12+
* The components within the modal
13+
* @type {PartialModalActionRow[]} The components in the modal
14+
*/
15+
this.components = components;
16+
}
17+
18+
/**
19+
* The extracted fields from the modal
20+
* @type {PartialInputTextData[]} The fields in the modal
21+
* @private
22+
*/
23+
get _fields() {
24+
return this.components.reduce((previous, next) => previous.concat(next.components), []);
25+
}
26+
27+
/**
28+
* Gets a field given a custom id from a component
29+
* @param {string} customId The custom id of the component
30+
* @returns {?PartialInputTextData}
31+
*/
32+
getField(customId) {
33+
const field = this._fields.find(f => f.customId === customId);
34+
if (!field) throw new TypeError('MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND', customId);
35+
return field;
36+
}
37+
38+
/**
39+
* Gets the value of a text input component given a custom id
40+
* @param {string} customId The custom id of the text input component
41+
* @returns {?string}
42+
*/
43+
getTextInputValue(customId) {
44+
const field = this.getField(customId);
45+
const expectedType = MessageComponentTypes[MessageComponentTypes.TEXT_INPUT];
46+
if (field.type !== expectedType) {
47+
throw new TypeError('MODAL_SUBMIT_INTERACTION_FIELD_TYPE', customId, field.type, expectedType);
48+
}
49+
return field.value;
50+
}
51+
}
52+
53+
module.exports = ModalSubmitFieldsResolver;
+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use strict';
2+
3+
const Interaction = require('./Interaction');
4+
const InteractionWebhook = require('./InteractionWebhook');
5+
const ModalSubmitFieldsResolver = require('./ModalSubmitFieldsResolver');
6+
const InteractionResponses = require('./interfaces/InteractionResponses');
7+
const { MessageComponentTypes } = require('../util/Constants');
8+
9+
/**
10+
* Represents a modal submit interaction.
11+
* @extends {Interaction}
12+
* @implements {InteractionResponses}
13+
*/
14+
class ModalSubmitInteraction extends Interaction {
15+
constructor(client, data) {
16+
super(client, data);
17+
18+
/**
19+
* The custom id of the modal.
20+
* @type {string}
21+
*/
22+
this.customId = data.data.custom_id;
23+
24+
/**
25+
* @typedef {Object} PartialTextInputData
26+
* @property {string} [customId] A unique string to be sent in the interaction when submitted
27+
* @property {MessageComponentType} [type] The type of this component
28+
* @property {string} [value] Value of this text input component
29+
*/
30+
31+
/**
32+
* @typedef {Object} PartialModalActionRow
33+
* @property {MessageComponentType} [type] The type of this component
34+
* @property {PartialTextInputData[]} [components] Partial text input components
35+
*/
36+
37+
/**
38+
* The inputs within the modal
39+
* @type {PartialModalActionRow[]}
40+
*/
41+
this.components =
42+
data.data.components?.map(c => ({
43+
type: MessageComponentTypes[c.type],
44+
components: ModalSubmitInteraction.transformComponent(c),
45+
})) ?? [];
46+
47+
/**
48+
* The message associated with this interaction
49+
* @type {Message|APIMessage|null}
50+
*/
51+
this.message = data.message ? this.channel?.messages._add(data.message) ?? data.message : null;
52+
53+
/**
54+
* The fields within the modal
55+
* @type {ModalSubmitFieldsResolver}
56+
*/
57+
this.fields = new ModalSubmitFieldsResolver(this.components);
58+
59+
/**
60+
* Whether the reply to this interaction has been deferred
61+
* @type {boolean}
62+
*/
63+
this.deferred = false;
64+
65+
/**
66+
* Whether the reply to this interaction is ephemeral
67+
* @type {?boolean}
68+
*/
69+
this.ephemeral = null;
70+
71+
/**
72+
* Whether this interaction has already been replied to
73+
* @type {boolean}
74+
*/
75+
this.replied = false;
76+
77+
/**
78+
* An associated interaction webhook, can be used to further interact with this interaction
79+
* @type {InteractionWebhook}
80+
*/
81+
this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token);
82+
}
83+
84+
/**
85+
* Transforms component data to discord.js-compatible data
86+
* @param {*} rawComponent The data to transform
87+
* @returns {PartialTextInputData[]}
88+
*/
89+
static transformComponent(rawComponent) {
90+
return rawComponent.components.map(c => ({
91+
value: c.value,
92+
type: MessageComponentTypes[c.type],
93+
customId: c.custom_id,
94+
}));
95+
}
96+
97+
// These are here only for documentation purposes - they are implemented by InteractionResponses
98+
/* eslint-disable no-empty-function */
99+
deferReply() {}
100+
reply() {}
101+
fetchReply() {}
102+
editReply() {}
103+
deleteReply() {}
104+
followUp() {}
105+
update() {}
106+
deferUpdate() {}
107+
}
108+
109+
InteractionResponses.applyToClass(ModalSubmitInteraction, ['showModal', 'awaitModalSubmit']);
110+
111+
module.exports = ModalSubmitInteraction;

‎src/structures/TextInputComponent.js

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
'use strict';
2+
3+
const BaseMessageComponent = require('./BaseMessageComponent');
4+
const { RangeError } = require('../errors');
5+
const { TextInputStyles, MessageComponentTypes } = require('../util/Constants');
6+
const Util = require('../util/Util');
7+
8+
/**
9+
* Represents a text input component in a modal
10+
* @extends {BaseMessageComponent}
11+
*/
12+
13+
class TextInputComponent extends BaseMessageComponent {
14+
/**
15+
* @typedef {BaseMessageComponentOptions} TextInputComponentOptions
16+
* @property {string} [customId] A unique string to be sent in the interaction when submitted
17+
* @property {string} [label] The text to be displayed above this text input component
18+
* @property {number} [maxLength] Maximum length of text that can be entered
19+
* @property {number} [minLength] Minimum length of text required to be entered
20+
* @property {string} [placeholder] Custom placeholder text to display when no text is entered
21+
* @property {boolean} [required] Whether or not this text input component is required
22+
* @property {TextInputStyleResolvable} [style] The style of this text input component
23+
* @property {string} [value] Value of this text input component
24+
*/
25+
26+
/**
27+
* @param {TextInputComponent|TextInputComponentOptions} [data={}] TextInputComponent to clone or raw data
28+
*/
29+
constructor(data = {}) {
30+
super({ type: 'TEXT_INPUT' });
31+
32+
this.setup(data);
33+
}
34+
35+
setup(data) {
36+
/**
37+
* A unique string to be sent in the interaction when submitted
38+
* @type {?string}
39+
*/
40+
this.customId = data.custom_id ?? data.customId ?? null;
41+
42+
/**
43+
* The text to be displayed above this text input component
44+
* @type {?string}
45+
*/
46+
this.label = data.label ?? null;
47+
48+
/**
49+
* Maximum length of text that can be entered
50+
* @type {?number}
51+
*/
52+
this.maxLength = data.max_length ?? data.maxLength ?? null;
53+
54+
/**
55+
* Minimum length of text required to be entered
56+
* @type {?string}
57+
*/
58+
this.minLength = data.min_length ?? data.minLength ?? null;
59+
60+
/**
61+
* Custom placeholder text to display when no text is entered
62+
* @type {?string}
63+
*/
64+
this.placeholder = data.placeholder ?? null;
65+
66+
/**
67+
* Whether or not this text input component is required
68+
* @type {?boolean}
69+
*/
70+
this.required = data.required ?? false;
71+
72+
/**
73+
* The style of this text input component
74+
* @type {?TextInputStyle}
75+
*/
76+
this.style = data.style ? TextInputComponent.resolveStyle(data.style) : null;
77+
78+
/**
79+
* Value of this text input component
80+
* @type {?string}
81+
*/
82+
this.value = data.value ?? null;
83+
}
84+
85+
/**
86+
* Sets the custom id of this text input component
87+
* @param {string} customId A unique string to be sent in the interaction when submitted
88+
* @returns {TextInputComponent}
89+
*/
90+
setCustomId(customId) {
91+
this.customId = Util.verifyString(customId, RangeError, 'TEXT_INPUT_CUSTOM_ID');
92+
return this;
93+
}
94+
95+
/**
96+
* Sets the label of this text input component
97+
* @param {string} label The text to be displayed above this text input component
98+
* @returns {TextInputComponent}
99+
*/
100+
setLabel(label) {
101+
this.label = Util.verifyString(label, RangeError, 'TEXT_INPUT_LABEL');
102+
return this;
103+
}
104+
105+
/**
106+
* Sets the text input component to be required for modal submission
107+
* @param {boolean} [required=true] Whether this text input component is required
108+
* @returns {TextInputComponent}
109+
*/
110+
setRequired(required = true) {
111+
this.required = required;
112+
return this;
113+
}
114+
115+
/**
116+
* Sets the maximum length of text input required in this text input component
117+
* @param {number} maxLength Maximum length of text to be required
118+
* @returns {TextInputComponent}
119+
*/
120+
setMaxLength(maxLength) {
121+
this.maxLength = maxLength;
122+
return this;
123+
}
124+
125+
/**
126+
* Sets the minimum length of text input required in this text input component
127+
* @param {number} minLength Minimum length of text to be required
128+
* @returns {TextInputComponent}
129+
*/
130+
setMinLength(minLength) {
131+
this.minLength = minLength;
132+
return this;
133+
}
134+
135+
/**
136+
* Sets the placeholder of this text input component
137+
* @param {string} placeholder Custom placeholder text to display when no text is entered
138+
* @returns {TextInputComponent}
139+
*/
140+
setPlaceholder(placeholder) {
141+
this.placeholder = Util.verifyString(placeholder, RangeError, 'TEXT_INPUT_PLACEHOLDER');
142+
return this;
143+
}
144+
145+
/**
146+
* Sets the style of this text input component
147+
* @param {TextInputStyleResolvable} style The style of this text input component
148+
* @returns {TextInputComponent}
149+
*/
150+
setStyle(style) {
151+
this.style = TextInputComponent.resolveStyle(style);
152+
return this;
153+
}
154+
155+
/**
156+
* Sets the value of this text input component
157+
* @param {string} value Value of this text input component
158+
* @returns {TextInputComponent}
159+
*/
160+
setValue(value) {
161+
this.value = Util.verifyString(value, RangeError, 'TEXT_INPUT_VALUE');
162+
return this;
163+
}
164+
165+
/**
166+
* Transforms the text input component into a plain object
167+
* @returns {APITextInput} The raw data of this text input component
168+
*/
169+
toJSON() {
170+
return {
171+
custom_id: this.customId,
172+
label: this.label,
173+
max_length: this.maxLength,
174+
min_length: this.minLength,
175+
placeholder: this.placeholder,
176+
required: this.required,
177+
style: TextInputStyles[this.style],
178+
type: MessageComponentTypes[this.type],
179+
value: this.value,
180+
};
181+
}
182+
183+
/**
184+
* Data that can be resolved to a TextInputStyle. This can be
185+
* * TextInputStyle
186+
* * number
187+
* @typedef {number|TextInputStyle} TextInputStyleResolvable
188+
*/
189+
190+
/**
191+
* Resolves the style of a text input component
192+
* @param {TextInputStyleResolvable} style The style to resolve
193+
* @returns {TextInputStyle}
194+
* @private
195+
*/
196+
static resolveStyle(style) {
197+
return typeof style === 'string' ? style : TextInputStyles[style];
198+
}
199+
}
200+
201+
module.exports = TextInputComponent;

‎src/structures/interfaces/InteractionResponses.js

+55-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
'use strict';
22

33
const { Error } = require('../../errors');
4-
const { InteractionResponseTypes } = require('../../util/Constants');
4+
const { InteractionResponseTypes, InteractionTypes } = require('../../util/Constants');
55
const MessageFlags = require('../../util/MessageFlags');
6+
const InteractionCollector = require('../InteractionCollector');
67
const MessagePayload = require('../MessagePayload');
8+
const Modal = require('../Modal');
79

810
/**
911
* Interface for classes that support shared interaction response types.
@@ -226,6 +228,56 @@ class InteractionResponses {
226228
return options.fetchReply ? this.fetchReply() : undefined;
227229
}
228230

231+
/**
232+
* Shows a modal component
233+
* @param {Modal|ModalOptions} modal The modal to show
234+
* @returns {Promise<void>}
235+
*/
236+
async showModal(modal) {
237+
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
238+
239+
const _modal = modal instanceof Modal ? modal : new Modal(modal);
240+
await this.client.api.interactions(this.id, this.token).callback.post({
241+
data: {
242+
type: InteractionResponseTypes.MODAL,
243+
data: _modal.toJSON(),
244+
},
245+
});
246+
this.replied = true;
247+
}
248+
249+
/**
250+
* An object containing the same properties as CollectorOptions, but a few more:
251+
* @typedef {Object} AwaitModalSubmitOptions
252+
* @property {CollectorFilter} [filter] The filter applied to this collector
253+
* @property {number} time Time to wait for an interaction before rejecting
254+
*/
255+
256+
/**
257+
* Collects a single modal submit interaction that passes the filter.
258+
* The Promise will reject if the time expires.
259+
* @param {AwaitModalSubmitOptions} options Options to pass to the internal collector
260+
* @returns {Promise<ModalSubmitInteraction>}
261+
* @example
262+
* // Collect a modal submit interaction
263+
* const filter = (interaction) => interaction.customId === 'modal';
264+
* interaction.awaitModalSubmit({ filter, time: 15_000 })
265+
* .then(interaction => console.log(`${interaction.customId} was submitted!`))
266+
* .catch(console.error);
267+
*/
268+
awaitModalSubmit(options) {
269+
if (typeof options.time !== 'number') throw new Error('INVALID_TYPE', 'time', 'number');
270+
const _options = { ...options, max: 1, interactionType: InteractionTypes.MODAL_SUBMIT };
271+
return new Promise((resolve, reject) => {
272+
const collector = new InteractionCollector(this.client, _options);
273+
collector.once('end', (interactions, reason) => {
274+
const interaction = interactions.first();
275+
if (interaction) resolve(interaction);
276+
else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason));
277+
});
278+
});
279+
}
280+
229281
static applyToClass(structure, ignore = []) {
230282
const props = [
231283
'deferReply',
@@ -236,6 +288,8 @@ class InteractionResponses {
236288
'followUp',
237289
'deferUpdate',
238290
'update',
291+
'showModal',
292+
'awaitModalSubmit',
239293
];
240294

241295
for (const prop of props) {

‎src/util/Constants.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,7 @@ exports.InteractionTypes = createEnum([
10761076
'APPLICATION_COMMAND',
10771077
'MESSAGE_COMPONENT',
10781078
'APPLICATION_COMMAND_AUTOCOMPLETE',
1079+
'MODAL_SUBMIT',
10791080
]);
10801081

10811082
/**
@@ -1099,6 +1100,7 @@ exports.InteractionResponseTypes = createEnum([
10991100
'DEFERRED_MESSAGE_UPDATE',
11001101
'UPDATE_MESSAGE',
11011102
'APPLICATION_COMMAND_AUTOCOMPLETE_RESULT',
1103+
'MODAL',
11021104
]);
11031105

11041106
/**
@@ -1109,7 +1111,7 @@ exports.InteractionResponseTypes = createEnum([
11091111
* @typedef {string} MessageComponentType
11101112
* @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types}
11111113
*/
1112-
exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU']);
1114+
exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU', 'TEXT_INPUT']);
11131115

11141116
/**
11151117
* The style of a message button
@@ -1152,6 +1154,15 @@ exports.NSFWLevels = createEnum(['DEFAULT', 'EXPLICIT', 'SAFE', 'AGE_RESTRICTED'
11521154
*/
11531155
exports.PrivacyLevels = createEnum([null, 'PUBLIC', 'GUILD_ONLY']);
11541156

1157+
/**
1158+
* The style of a text input component
1159+
* * SHORT
1160+
* * PARAGRAPH
1161+
* @typedef {string} TextInputStyle
1162+
* @see {@link https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-styles}
1163+
*/
1164+
exports.TextInputStyles = createEnum([null, 'SHORT', 'PARAGRAPH']);
1165+
11551166
/**
11561167
* Privacy level of a {@link GuildScheduledEvent} object:
11571168
* * GUILD_ONLY

‎typings/enums.d.ts

+13
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,15 @@ export const enum InteractionResponseTypes {
111111
DEFERRED_MESSAGE_UPDATE = 6,
112112
UPDATE_MESSAGE = 7,
113113
APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8,
114+
MODAL = 9,
114115
}
115116

116117
export const enum InteractionTypes {
117118
PING = 1,
118119
APPLICATION_COMMAND = 2,
119120
MESSAGE_COMPONENT = 3,
120121
APPLICATION_COMMAND_AUTOCOMPLETE = 4,
122+
MODAL_SUBMIT = 5,
121123
}
122124

123125
export const enum InviteTargetType {
@@ -142,6 +144,12 @@ export const enum MessageComponentTypes {
142144
ACTION_ROW = 1,
143145
BUTTON = 2,
144146
SELECT_MENU = 3,
147+
TEXT_INPUT = 4,
148+
}
149+
150+
export const enum ModalComponentTypes {
151+
ACTION_ROW = 1,
152+
TEXT_INPUT = 4,
145153
}
146154

147155
export const enum MFALevels {
@@ -184,6 +192,11 @@ export const enum StickerTypes {
184192
GUILD = 2,
185193
}
186194

195+
export const enum TextInputStyles {
196+
SHORT = 1,
197+
PARAGRAPH = 2,
198+
}
199+
187200
export const enum VerificationLevels {
188201
NONE = 0,
189202
LOW = 1,

‎typings/index.d.ts

+207-31
Large diffs are not rendered by default.

‎typings/index.test-d.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,8 @@ client.on('interaction', async interaction => {
679679

680680
void new MessageActionRow();
681681

682+
void new MessageActionRow({});
683+
682684
const button = new MessageButton();
683685

684686
const actionRow = new MessageActionRow({ components: [button] });
@@ -688,9 +690,6 @@ client.on('interaction', async interaction => {
688690
// @ts-expect-error
689691
interaction.reply({ content: 'Hi!', components: [[button]] });
690692

691-
// @ts-expect-error
692-
void new MessageActionRow({});
693-
694693
// @ts-expect-error
695694
await interaction.reply({ content: 'Hi!', components: [button] });
696695

‎typings/rawDataTypes.d.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,13 @@ import {
7676
RESTPostAPIWebhookWithTokenJSONBody,
7777
Snowflake,
7878
APIGuildScheduledEvent,
79+
APIActionRowComponent,
80+
APITextInputComponent,
81+
APIModalActionRowComponent,
82+
APIModalSubmitInteraction,
7983
} from 'discord-api-types/v9';
80-
import { GuildChannel, Guild, PermissionOverwrites } from '.';
84+
import { GuildChannel, Guild, PermissionOverwrites, InteractionType } from '.';
85+
import type { InteractionTypes, MessageComponentTypes } from './enums';
8186

8287
export type RawActivityData = GatewayActivity;
8388

@@ -141,6 +146,9 @@ export type RawMessageComponentInteractionData = APIMessageComponentInteraction;
141146
export type RawMessageButtonInteractionData = APIMessageButtonInteractionData;
142147
export type RawMessageSelectMenuInteractionData = APIMessageSelectMenuInteractionData;
143148

149+
export type RawTextInputComponentData = APITextInputComponent;
150+
export type RawModalSubmitInteractionData = APIModalSubmitInteraction;
151+
144152
export type RawInviteData =
145153
| APIExtendedInvite
146154
| APIInvite

0 commit comments

Comments
 (0)
Please sign in to comment.