From 8eee1d09a87b98c8484f2fece32c9551dbbc6dd2 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 19 Mar 2025 21:13:28 +0100 Subject: [PATCH 1/2] 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 --- public/scripts/slash-commands.js | 78 ++++++++++++++++++++++++++------ public/style.css | 31 ++++++++++++- 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 096da917a..7b6678c1d 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.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,18 @@ 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) { + return JSON.stringify(Array.from(multipleToggledState).map(r => resultToButtonMap.get(r) ?? '')); + } + 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 { From 40f2eae3f3297856d799c5789d3fcc3e67e25be8 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 19 Mar 2025 22:46:11 +0100 Subject: [PATCH 2/2] Make it return empty array on cancel --- public/scripts/slash-commands.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 7b6678c1d..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'; @@ -2474,7 +2474,8 @@ async function buttonsCallback(args, text) { /** @returns {string} @param {string|number|boolean} result */ function getResult(result) { if (multiple) { - return JSON.stringify(Array.from(multipleToggledState).map(r => resultToButtonMap.get(r) ?? '')); + 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) ?? '' : ''; }