Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,8 +541,8 @@ registerMenu("menu", useComponentsV2 = true) {
+container(color = 0x00FF00) {
+section(
modalButton(
"text", label = "Modal", title = "Enter Text", component =
textInput("text", placeholder = "Hello World!", value = text).withLabel("Enter Text")
"text", label = "Modal", title = "Enter Text",
component = textInput("text", placeholder = "Hello World!", value = text).withLabel("Enter Text")
) {
text = it
}
Expand Down Expand Up @@ -772,11 +772,15 @@ registerModal<Unit>("modal") {
> You always have to wrap components in a `label` to be able to add them to a modal menu. There is also the `withLabel` extension function that makes the component creation more readable.

> [!NOTE]
> Discord currently only allows `textInput`s, `stringSelect`s and `entitySelect`s inside modals. For select menus, their main handler (and option handlers) are NOT executed. Instead, the `modalHandler` is used.
> Discord currently only allows `textInput`s, `stringSelect`s, `entitySelect`s and checkbox components inside modals. For select menus, their main handler (and option handlers) are NOT executed. Instead, the `modalHandler` is used.

You can create text inputs with the `textInput` function. They can be added to your modal with the `+` operator. Adding a text input will return a function that can be used inside the executor block to read the inputs value.
The `execute` block is what is executed when a user submits a modal.

In addition to `textInput` there are also the `checkbox`, `checkboxGroup` and `radioGroup` components, that are exclusive to modals. They also have factory functions as usual.
While discord itself does not support required checkboxes, discord tool kit provides a `requiredCheckbox` function, that creates a required checkbox group with a single option under the hood.
This is useful for additional confirmation steps, since users cannot submit the modal unless they check the box.

If you only want to use a modal to read a text input from the user in a message menu, you can use `modalButton` (or `modalSelectOption`):
```kt
registerMenu<Unit>("menu", deferMode = DeferMode.UNLESS_PREVENTED) {
Expand Down
39 changes: 39 additions & 0 deletions examples/src/main/kotlin/examples/CheckboxModal.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package examples

import de.mineking.discord.commands.menuCommand
import de.mineking.discord.discordToolKit
import de.mineking.discord.ui.builder.components.message.actionRow
import de.mineking.discord.ui.builder.components.message.button
import de.mineking.discord.ui.builder.components.modal.checkboxGroupOption
import de.mineking.discord.ui.builder.components.modal.radioGroup
import de.mineking.discord.ui.builder.components.modal.requiredCheckbox
import de.mineking.discord.ui.builder.components.modal.withLabel
import de.mineking.discord.ui.message.modal
import de.mineking.discord.ui.modal.getValue
import setup.createJDA

fun main() {
val jda = createJDA()
discordToolKit(jda)
.withUIManager()
.withCommandManager {
+menuCommand("checkbox") {
val modal = modal("checkbox") {
val selected by +radioGroup("test", checkboxGroupOption("a"), checkboxGroupOption("b")).withLabel("Radio Group")
+requiredCheckbox("require").withLabel("I confirm that this action cannot be undone")

execute {
reply("Selected: $selected").setEphemeral(true).queue()
}
}

+actionRow(
button("open", label = "open") {
switchMenu(modal)
}
)
}

updateCommands().queue()
}.build()
}
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ kotlin-serialization = "1.9.0"
kotlin-coroutines = "1.10.2"

dtk-localization = "1.1.0"
jda = "6.3.1"
jda = "6.4.1"
emoji = "2.3.0"

logging = "2.0.11"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package de.mineking.discord.ui.builder.components.modal

import de.mineking.discord.localization.DEFAULT_LABEL
import de.mineking.discord.localization.LocalizationFile
import de.mineking.discord.ui.MenuConfig
import de.mineking.discord.ui.modal.ModalResultHandler
import de.mineking.discord.ui.modal.createModalElement
import de.mineking.discord.ui.modal.map
import de.mineking.discord.ui.readLocalizedString
import net.dv8tion.jda.api.EmbedBuilder.ZERO_WIDTH_SPACE
import net.dv8tion.jda.api.components.checkbox.Checkbox
import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroup
import net.dv8tion.jda.api.components.radiogroup.RadioGroup
import kotlin.math.absoluteValue
import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroupOption as JDACheckboxGroupOption
import net.dv8tion.jda.api.components.radiogroup.RadioGroupOption as JDARadioGroupOption

fun checkbox(
name: String,
default: Boolean = false,
handler: ModalResultHandler<Boolean>? = null
) = createModalElement(name, {
val temp = event.getValueByUniqueId(name.hashCode().absoluteValue)!!.asBoolean
temp.also { handler?.invoke(this, it) }
}) { _, id ->
Checkbox.of(id, default)
.withUniqueId(name.hashCode().absoluteValue)
}

class CheckboxGroupOption(
val value: String,
val label: CharSequence,
val description: CharSequence?,
val default: Boolean,
val localization: LocalizationFile?,
val visible: Boolean,
) {
fun buildCheckboxOption(name: String, localization: LocalizationFile?, config: MenuConfig<*, *>) =
JDACheckboxGroupOption.of(config.readLocalizedString(this.localization ?: localization, name, label, "label", postfix = "options.$value") ?: ZERO_WIDTH_SPACE, value)
.withDescription(config.readLocalizedString(this.localization ?: localization, name, description, "description", postfix = "options.$value"))
.withDefault(default)
.withValue(value)

fun buildRadioOption(name: String, localization: LocalizationFile?, config: MenuConfig<*, *>) =
JDARadioGroupOption.of(config.readLocalizedString(this.localization ?: localization, name, label, "label", postfix = "options.$value") ?: ZERO_WIDTH_SPACE, value)
.withDescription(config.readLocalizedString(this.localization ?: localization, name, description, "description", postfix = "options.$value"))
.withDefault(default)
.withValue(value)
}

fun CheckboxGroupOption.visibleIf(visible: Boolean) = CheckboxGroupOption(value, label, description, default, localization, visible)
fun CheckboxGroupOption.hiddenIf(hide: Boolean) = visibleIf(!hide)

fun checkboxGroupOption(
value: Any,
label: CharSequence = DEFAULT_LABEL,
description: CharSequence? = DEFAULT_LABEL,
default: Boolean = false,
localization: LocalizationFile? = null,
) = CheckboxGroupOption(value.toString(), label, description, default, localization, true)

fun checkboxGroup(
name: String,
options: List<CheckboxGroupOption>,
required: Boolean = false,
minValues: Int? = null,
maxValues: Int? = null,
localization: LocalizationFile? = null,
handler: ModalResultHandler<List<String>>? = null,
) = createModalElement(name, {
val temp = event.getValueByUniqueId(name.hashCode().absoluteValue)!!.asStringList
temp.also { handler?.invoke(this, it) }
}) { config, id ->
CheckboxGroup.create(id)
.setUniqueId(name.hashCode().absoluteValue)
.setRequired(required)
.addOptions(options.filter { it.visible }.map { it.buildCheckboxOption(name, localization, config) })
.apply {
if (minValues != null) setMinValues(minValues)
if (maxValues != null) setMaxValues(maxValues)
}
.build()
}

fun checkboxGroup(
name: String,
vararg options: CheckboxGroupOption,
required: Boolean = false,
minValues: Int? = null,
maxValues: Int? = null,
localization: LocalizationFile? = null,
handler: ModalResultHandler<List<String>>? = null,
) = checkboxGroup(name, options.toList(), required, minValues, maxValues, localization, handler)

fun radioGroup(
name: String,
options: List<CheckboxGroupOption>,
required: Boolean = false,
localization: LocalizationFile? = null,
handler: ModalResultHandler<String>? = null,
) = createModalElement(name, {
val temp = event.getValueByUniqueId(name.hashCode().absoluteValue)!!.asString
temp.also { handler?.invoke(this, it) }
}) { config, id ->
RadioGroup.create(id)
.setUniqueId(name.hashCode().absoluteValue)
.setRequired(required)
.addOptions(options.filter { it.visible }.map { it.buildRadioOption(name, localization, config) })
.build()
}

fun radioGroup(
name: String,
vararg options: CheckboxGroupOption,
required: Boolean = false,
localization: LocalizationFile? = null,
handler: ModalResultHandler<String>? = null,
) = radioGroup(name, options.toList(), required, localization, handler)

fun requiredCheckbox(
name: String,
default: Boolean = false,
) = checkboxGroup(
name,
checkboxGroupOption("value", default = default),
required = true,
minValues = 1,
).map { "value" in it }
Loading