Add support for toggleable /buttons

Enhances the buttons slash command with toggleable multi-select capability
Introduces a 'multiple' parameter to enable selecting several options
Updates button styling with visual indicators for toggle states
Modifies return value to handle array of selections when multiple is enabled
This commit is contained in:
Wolfsblvt
2025-03-19 21:13:28 +01:00
parent c047e1acd9
commit 8eee1d09a8
2 changed files with 94 additions and 15 deletions

View File

@ -77,6 +77,7 @@ import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHel
import { accountStorage } from './util/AccountStorage.js'; import { accountStorage } from './util/AccountStorage.js';
import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js'; import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
import { t } from './i18n.js';
export { export {
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
}; };
@ -1556,16 +1557,28 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'buttons', name: 'buttons',
callback: buttonsCallback, callback: buttonsCallback,
returns: 'clicked button label', returns: 'clicked button label (or array of labels if multiple is enabled)',
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( SlashCommandNamedArgument.fromProps({
'labels', 'button labels', [ARGUMENT_TYPE.LIST], true, 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: [ unnamedArgumentList: [
new SlashCommandArgument( SlashCommandArgument.fromProps({
'text', [ARGUMENT_TYPE.STRING], true, description: 'text',
), typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
}),
], ],
helpString: ` helpString: `
<div> <div>
@ -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<string>} - 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) { async function buttonsCallback(args, text) {
try { try {
/** @type {string[]} */ /** @type {string[]} */
@ -2387,6 +2412,10 @@ async function buttonsCallback(args, text) {
return ''; return '';
} }
/** @type {Set<number>} */
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 // 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])); 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) { for (const [result, button] of resultToButtonMap) {
const buttonElement = document.createElement('div'); const buttonElement = document.createElement('div');
buttonElement.classList.add('menu_button', 'result-control', 'wide100p'); buttonElement.classList.add('menu_button', 'wide100p');
buttonElement.dataset.result = String(result);
buttonElement.addEventListener('click', async () => { if (multiple) {
await popup.complete(result); 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; buttonElement.innerText = button;
buttonContainer.appendChild(buttonElement); buttonContainer.appendChild(buttonElement);
} }
@ -2424,10 +2466,18 @@ async function buttonsCallback(args, text) {
popupContainer.style.flexDirection = 'column'; popupContainer.style.flexDirection = 'column';
popupContainer.style.maxHeight = '80vh'; // Limit the overall height of the popup 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() popup.show()
.then((result => resolve(typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : ''))) .then((result => resolve(getResult(result))))
.catch(() => resolve('')); .catch(() => resolve(''));
/** @returns {string} @param {string|number|boolean} result */
function getResult(result) {
if (multiple) {
return JSON.stringify(Array.from(multipleToggledState).map(r => resultToButtonMap.get(r) ?? ''));
}
return typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : '';
}
}); });
} catch { } catch {
return ''; return '';

View File

@ -2911,6 +2911,35 @@ select option:not(:checked) {
pointer-events: none; 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 { .fav_on {
color: var(--golden) !important; color: var(--golden) !important;
} }
@ -2921,7 +2950,7 @@ select option:not(:checked) {
} }
.menu_button.togglable:not(.toggleEnabled) { .menu_button.togglable:not(.toggleEnabled) {
color: red; color: var(--fullred);
} }
.displayBlock { .displayBlock {