diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 096da917a..969348ae5 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -69,7 +69,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
-import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
+import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
@@ -77,6 +77,7 @@ import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHel
import { accountStorage } from './util/AccountStorage.js';
import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
+import { t } from './i18n.js';
export {
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
};
@@ -1556,16 +1557,28 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'buttons',
callback: buttonsCallback,
- returns: 'clicked button label',
+ returns: 'clicked button label (or array of labels if multiple is enabled)',
namedArgumentList: [
- new SlashCommandNamedArgument(
- 'labels', 'button labels', [ARGUMENT_TYPE.LIST], true,
- ),
+ SlashCommandNamedArgument.fromProps({
+ name: 'labels',
+ description: 'button labels',
+ typeList: [ARGUMENT_TYPE.LIST],
+ isRequired: true,
+ }),
+ SlashCommandNamedArgument.fromProps({
+ name: 'multiple',
+ description: 'if enabled multiple buttons can be clicked/toggled, and all clicked buttons are returned as an array',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ enumList: commonEnumProviders.boolean('trueFalse')(),
+ defaultValue: 'false',
+ }),
],
unnamedArgumentList: [
- new SlashCommandArgument(
- 'text', [ARGUMENT_TYPE.STRING], true,
- ),
+ SlashCommandArgument.fromProps({
+ description: 'text',
+ typeList: [ARGUMENT_TYPE.STRING],
+ isRequired: true,
+ }),
],
helpString: `
@@ -2377,6 +2390,18 @@ async function trimTokensCallback(arg, value) {
}
}
+/**
+ * Displays a popup with buttons based on provided labels and handles button interactions.
+ *
+ * @param {object} args - Named arguments for the command
+ * @param {string} args.labels - JSON string of an array of button labels
+ * @param {string} [args.multiple=false] - Flag indicating if multiple buttons can be toggled
+ * @param {string} text - The text content to be displayed within the popup
+ *
+ * @returns {Promise} - A promise that resolves to a string of the button labels selected
+ * If 'multiple' is true, returns a JSON string array of labels.
+ * If 'multiple' is false, returns a single label string.
+ */
async function buttonsCallback(args, text) {
try {
/** @type {string[]} */
@@ -2387,6 +2412,10 @@ async function buttonsCallback(args, text) {
return '';
}
+ /** @type {Set} */
+ const multipleToggledState = new Set();
+ const multiple = isTrueBoolean(args?.multiple);
+
// Map custom buttons to results. Start at 2 because 1 and 0 are reserved for ok and cancel
const resultToButtonMap = new Map(buttons.map((button, index) => [index + 2, button]));
@@ -2404,11 +2433,24 @@ async function buttonsCallback(args, text) {
for (const [result, button] of resultToButtonMap) {
const buttonElement = document.createElement('div');
- buttonElement.classList.add('menu_button', 'result-control', 'wide100p');
- buttonElement.dataset.result = String(result);
- buttonElement.addEventListener('click', async () => {
- await popup.complete(result);
- });
+ buttonElement.classList.add('menu_button', 'wide100p');
+
+ if (multiple) {
+ buttonElement.classList.add('toggleable');
+ buttonElement.dataset.toggleValue = String(result);
+ buttonElement.addEventListener('click', async () => {
+ buttonElement.classList.toggle('toggled');
+ if (buttonElement.classList.contains('toggled')) {
+ multipleToggledState.add(result);
+ } else {
+ multipleToggledState.delete(result);
+ }
+ });
+ } else {
+ buttonElement.classList.add('result-control');
+ buttonElement.dataset.result = String(result);
+ }
+
buttonElement.innerText = button;
buttonContainer.appendChild(buttonElement);
}
@@ -2424,10 +2466,19 @@ async function buttonsCallback(args, text) {
popupContainer.style.flexDirection = 'column';
popupContainer.style.maxHeight = '80vh'; // Limit the overall height of the popup
- popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel', allowVerticalScrolling: true });
+ popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: multiple ? t`Ok` : t`Cancel`, allowVerticalScrolling: true });
popup.show()
- .then((result => resolve(typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : '')))
+ .then((result => resolve(getResult(result))))
.catch(() => resolve(''));
+
+ /** @returns {string} @param {string|number|boolean} result */
+ function getResult(result) {
+ if (multiple) {
+ const array = result === POPUP_RESULT.AFFIRMATIVE ? Array.from(multipleToggledState).map(r => resultToButtonMap.get(r) ?? '') : [];
+ return JSON.stringify(array);
+ }
+ return typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : '';
+ }
});
} catch {
return '';
diff --git a/public/style.css b/public/style.css
index 7fa72d91b..13a1267ab 100644
--- a/public/style.css
+++ b/public/style.css
@@ -2911,6 +2911,35 @@ select option:not(:checked) {
pointer-events: none;
}
+.menu_button.toggleable {
+ padding-left: 20px;
+}
+
+.menu_button.toggleable.toggled {
+ border-color: var(--active);
+}
+
+.menu_button.toggleable:not(.toggled) {
+ filter: brightness(80%);
+}
+
+.menu_button.toggleable::before {
+ font-family: "Font Awesome 6 Free";
+ margin-left: 10px;
+ position: absolute;
+ left: 0;
+}
+
+.menu_button.toggleable.toggled::before {
+ content: "\f00c";
+ color: var(--active);
+}
+
+.menu_button.toggleable:not(.toggled)::before {
+ content: "\f00d";
+ color: var(--fullred);
+}
+
.fav_on {
color: var(--golden) !important;
}
@@ -2921,7 +2950,7 @@ select option:not(:checked) {
}
.menu_button.togglable:not(.toggleEnabled) {
- color: red;
+ color: var(--fullred);
}
.displayBlock {